[iOS] Initializer - 02

Sangwon Shin·2021년 12월 14일
0

iOS

목록 보기
5/9

📃 Initializer Delegation for Value Types

어제 이니셜라이저를 공부하면서 이니셜라이저를 포함한 클래스를 상속하는 경우에 대해서 살펴보다가 끝났습니다.

오늘은 완벽한 이해를 위해서 이니셜라이저 위임 부터 먼저 살펴보겠습니다.

💡 Value Types 을 위한 생성자 위임이라니 그럼 값 타입과 참조 타입에 따라 생성자 위임 방식이 다른건가요?

맞습니다.. 근데 조금만 생각해보면 쉽게 이유를 알 수 있습니다.
우리가 Value Type, Reference Type 을 공부했던게 구조체와 클래스의 차이였습니다. 그 둘의 가장 큰 차이점은 구조체는 상속이 불가능하고, 클래스는 상속이 가능했습니다.

❗️그래서 값타입, 참조타입 Initializer Delegation 이 다릅니다.

먼저 비교적 간단한 값타입 이니셜라이저 위임을 살펴보겠습니다.

struct Size {
    var width = 0.0
    var height = 0.0
}

struct Point {
    var x = 0.0
    var y = 0.0
}

struct Rect {
    var origin = Point()
    var size = Size()
    
    init() { }
    init(origin: Point, size: Size) {
        self.origin = origin
        self.size = size
    }
    
    init(center: Point, size: Size) {
        let originX = center.x - (size.width / 2)
        let originY = center.y - (size.height / 2)
        self.init(origin: Point(x: originX, y: originY), size: size)
    }
}

let sample1 = Rect()
let sample2 = Rect(origin: Point(x: 0, y: 0), size: Size(width: 10, height: 5))
let sample3 = Rect(center: Point(x: 5, y: 5), size: Size(width: 10, height: 10))

Swift 공식 홈페이지에 있는 예제를 살펴보겠습니다. 좌표와 크기를 이용해서 직사각형을 정의하고 있습니다.

구조체의 경우 우리가 별다른 이니셜라이저를 구현하지 않으면 멤버와이즈 이니셜라이저를 사용할 수 있습니다.

Size, Point 구조체의 경우 따로 이니셜라이저를 구현하지 않고, 디폴트 값을 설정했기 때문에 멤버와이즈 이니셜라이저 또는 디폴트값 을 통해서 인스턴스를 생성할 수 있습니다.

Rect 구조체의 경우에는 3가지 방법을 통해서 인스턴스를 생성 할 수 있습니다.

  • init()
  • init(origin: Point, size: Size)
  • init(center: Point, size: Size)

<첫번째 방법>
Rect 에서 사용하는 저장 프로퍼티 타입인 Size, Point 가 디폴트 값을 가지고 있기 때문에 (0,0) 좌표에 높이와 넓이가 0인 직사각형을 의미합니다.

<두번째 방법>
사용자로부터 Origin(좌표), size(크기) 를 입력받아서 직사각형을 정의합니다. Point, Size 타입을 멤버와이즈 이니셜라이저를 통해 생성하거나 디폴트값을 이용 할 수 있습니다.

<세번째 방법>
사용자로 부터 직사각형의 center(중간좌표), 크기(size) 를 입력받아서 직사각형을 정의합니다. 이때, 두번째 이니셜라이저를 사용합니다.

값 타입의 이니셜라이저는 상속을 지원하지 않기 때문에 세 번째 방법처럼, 이니셜라이저를 자신의 다른 이니셜라이저에서만 사용 가능합니다.


📑 Initalizer Delegation for Class Types

어제 블로그에서 Designated Initalizer, Convenience Initalize 에 대해서 아주 간단하게 정리했었습니다.

Designated Initalizer(지정 이니셜라이저) 는 우리가 일반적으로 사용했던 모든 프로퍼티를 초기화 하는 이니셜라이저

Convenience Initalize(편의 이니셜라이저) 는 기존의 이니셜라이저의 파라미터 중 일부를 기본값으로 사용해 초기화 할 수 있는 이니셜라이져

좀 더 추가적으로 정리를 해보자면,

  • 지정 이니셜라이저는 정의된 클래스의 모든 프로퍼티를 초기화해야만 합니다.
  • 지정 이니셜라이저는 클래스의 이니셜라이저 중 기둥과 같은 역할을 하기 때문에 클래스에 하나 이상 정의해야 합니다.
두번째 조건에 의해서 모든 클래스는 하나 이상의 지정 이니셜라이저를 가져야 하지만, 부모 클래스의 지정 이니셜라이저가 자식 클래스의 이니셜라이저 역할을 할 수 있다면 생략할 수 있습니다. (저장 프로퍼티가 추가되지 않는 경우)

클래스 타입 이니셜라이저 위임에는 3가지 규칙이 있습니다.

  • 자식 클래스의 지정 이니셜라이저는 반드시 부모 클래스의 지정 이니셜라이저를 호출해야 한다.
  • 편의 이니셜라이저는 반드시 자신을 정의한 클래스의 다른 이니셜라이저를 호출해야 한다.
  • 편의 이니셜라이저는 궁극적으로 지정 이니셜라이저를 반드시 호출해야 한다.

1 ) 자식 클래스의 지정 이니셜라이저들은 모두 부모의 지정 이니셜라이저를 호출하고 있습니다.

2 ) 편의 이니셜라이저들은 결국 지정 이니셜라이저를 호출하고 있습니다.

왜 이런 규칙들이 있는 걸까요?

Swift는 클래스 이니셜라이저를 통해서 인스턴스를 생성할 때 2단계의 과정을 거치게 되기 때문입니다.
1단계는 인스턴스를 위한 메모리 공간 확보, 초기화 및 값 할당
2단계는 저장프로퍼티들을 사용자 정의(ex]self)

그리고 이런 2단계 과정을 오류없이 처리하기 위해서 4단계의 안전확인을 실행합니다.

자식클래스의 지정 이니셜라이저가 부모클래스의 이니셜라이저를 호출하기 전에 자신의 프로퍼티를 모두 초기화 했는가?

자식클래스의 지정 이니셔라이저는 상속받은 프로퍼티에 값을 할당하기 전에 반드시 부모 클래스의 이니셜라이저를 호출해야 한다.

편의 이니셜라이저는 자신의 클래스에 정의한 프로퍼티를 포함해 어떤 프로퍼티라도 값을 할당하기 전에 다른 이니셜라이저를 호출해야 한다.

1단계를 마치기전까지 이니셜라이저는 인스턴스 메서드와 인스턴스 프로퍼티 값, self 프로퍼티를 사용할 수 없다.

예제를 통해서 살펴보겠지만, 해당 클래스의 지정 이니셜라이저를 통해 해당 클래스에 존재하는 저장 프로퍼티값들을 위한 메모리 공간을 확보해야 초기화를 진행하고 값을 할당할 수 있다는 것에 중점을 두면 이해가 조금 더 쉬울것 같습니다.

<1단계>

  • 클래스가 이니셜라이저를 호출합니다.
  • 클래스의 인스턴스를 위한 메모리가 할당됩니다. (초기화 x)
  • 해당 클래스에 정의된 모든 저장 프로퍼티에 값이 있는 확인합니다. (현재 클래스 부분까지의 저장 프로퍼티를 위한 메모리는 초기화 o)
  • 지정이니셜라이저는 부모 클래스의 이니셜라이저가 같은 동작을 할 수 있도록 양도
  • 최상위 클래스에 도달할 때까지 반복

❗️ 자식 -> 부모로 저장 프로퍼티를 위한 메모리 공간이 확보되고 초기화 됩니다.

<2단계>

  • 최상위 클래스부터 하위 클래스까지 내려오면서 지정 이니셜라이저들이 인스턴스를 사용자 정의
  • 마지막으로 각각의 편의 이니셜라이저를 통해 self 작업 진행

자식 클래스의 편의 이니셜라이저를 통해서 인스턴스를 생성하는 경우를 확인해보겠습니다.

class Person {
    var name: String
    var age: Int
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

class Student: Person {
    var major: String
    
    init(name: String, age: Int, major: String) {
        self.major = "Electornic"
//        print(self.age)
        super.init(name: name, age: age)
        print(self.age)
    }
    
    convenience init(name: String) {
        self.init(name: name, age: 18, major: "highScholl")
    }
}

let sangwon = Student(name: "sangwon", age: 26, major: "")
let me = Student(name: "shin")

Student(name: "shin") 라는 인스턴스 생성하게 되면 편의 이니셜라이저는 Student 클래스의 지정 생성자를 호출하게 됩니다.

Student 클래스의 지정 생성자는 해당 클래스의 저장 프로퍼티인 major 를 초기화 하고, 부모 클래스의 지정 생성자 (super.init) 를 호출합니다.

부모 클래스의 지정 생성자를 통해 저장 프로퍼티 name, age 를 초기화 합니다.

어떤 프로퍼티를 초기화 하기 전에 접근하는 경우를 피해야 한다 라고 생각하고 살펴봅시다.
처음 편의 이니셜라이저를 호출하면 어떤 저장 프로퍼티를 위한 메모리가 확보되지 않은 상태이기 때문에 지정 이니셜라이저를 호출하기 전에 self 를 사용하면 오류가 발생합니다.

마찬가지로 student 클래스의 지정 이니셜라이저에서 부모의 지정 이니셜라이저를 호출하기 전에 부모 클래스의 저장 프로퍼티에 접근하면 오류가 발생합니다.


🏷 P.S.

스토리보드를 사용하지 않고 코드만 이용해서 뷰를 구성하고 오토 레이아웃을 설정하는 연습을 하고 있습니다.

오늘 연습하면서 다양한 버그들을 접했는데 까먹기 전에 블로그에 정리 할 수 있도록 하겠습니다. 생성자 멈춰🤚 ㅠ

아마 내일정도 까지는 생성자 정리를 계속해서 하게 될 것 같습니다..

  • 이니셜라이저의 상속과 오버라이딩
  • 실패 가능한 이니셜라이저
  • 필수 이니셜라이저

를 위주로 다음 게시글에 정리해보도록 하겠습니다.

profile
개발자가 되고싶어요

0개의 댓글