아리의 iOS 탐구생활

[Swift] 이니셜라이저(initalizer) / 참조타입에서의 사용 [3] 본문

Swift/문법

[Swift] 이니셜라이저(initalizer) / 참조타입에서의 사용 [3]

Ari Lee 2021. 8. 15. 03:25
반응형

✔️ Class’s initalizer

 

 

🔍  Designated initalizer (지정 초기화)

흔히 보는 기본적인 initalizer이다. 부모 Class의 initalizer를 호출할 수 있다.

class 내부에는 반드시 한 개 이상의 Designated initalizer가 있어야 한다.

init(매개 변수) {
	/* 구현부 */
}

 

 

 

🔍  Convenience initalizer (편의 초기화)

Designated initalizer의 일부 매개변수의 기본값을 설정하여 초기화하는 initalizer이다.

쉽게 말하면 기존 Designated initalizer에 default를 주고 싶은 경우에 사용한다.

더 적은 입력으로 초기화를 편리하게 할 수 있게 도와주는 역할을 한다.

convenience init() {
	/* 구현부 */
}

 

 

 

🔍  예제

class Person {
    var name: String
    var age: Int
    
    init(name: String, age: Int) { // Designated initalizer
        self.name = name
        self.age = age
    }
    
    convenience init(name: String) {
        self.init(name: name, age: 27) // Designated initalizer 호출
    }
}

let own = Person(name: "arirang", age: 27) // Designated initalizer
let two = Person(name: "Ari") // convenience initalizer

 

 

 

👉🏻  만약 Convenience initalizer을 생략하고 싶다면 Property에 기본값을 할당해주면 된다.

class Person {
    var name: String
    var age: Int = 27
    
    init(name: String) { // Designated initalizer
        self.name = name
    }

}

let ari = Person(name: "Ari")
print(ari.name, ari.age) // Ari 27

 

 

🔍  Required initalizer (필수 초기화)

생성자의 재정의를 필수적으로 해줘야 한다는 것을 알려주기 위한 이니셜라이저.

클래스에서 반드시 구현해야 하는 이니셜라이저이다. 앞에 required라는 키워드를 붙여 사용해야한다.

해당 클래스를 상속받은 클래스에서 구현할 때에도 required 키워드를 붙여 사용해야한다.

참고로 required는 오버라이드를 기본으로 포함하고 있다.

class A {
    required init() {
        // 구현부
    }
}

class B: A {
    required init() {
        // 자식 클래스의 required 이니셜라이저 구현
    }
}

 

 

 

🔍  deinitalizer (초기화 해제)

  • deinit은 클래스의 인스턴스가 메모리에서 해제되는 시점에 호출된다.
  • 인스턴스가 해제되는 시점에 해야할 일을 구현할 수 있다.
  • deinit은 매개변수를 지닐 수 없다.
  • 자동으로 호출되므로 직접 호출할 수 없다.
  • 클래스 타입에서만 구현할 수 있다.
  • 인스턴스가 메모리에서 해제되는 시점은 ARC의 규칙에 따라 결정된다.
class TestClass {
    var test: TestClass? = nil
    init() {
        print("init")
    }
    deinit {
        print("deinit")
    }
}

var test: TestClass? = TestClass() // init
test = nil // deinit

 

 

 

 

✔️ 참조타입의 initalizer Delegation

designated initalizerconvenience initalizer사이의 관계를 단순하게 하기 위해 Swift는 이니셜라이저는 위임하는 호출을 위해 다음 3가지 규칙을 따른다.

Rule 1 Designated initalizer는 반드시 부모 클래스의 이니셜라이저를 호출해야 한다.

Rule 2 Convenience initalizer는 반드시 같은 클래스의 이니셜라이저를 호출해야 한다.

Rule 3 Convenience initalizer 결과적으로 Designated initalizer 호출해야 한다.

 

쉽게 요약하면

  • Designated initalizer는 반드시 위임을 superclass로 해야한다. (위로)
  • Convenience initalizer는 반드시 위임을 같은 레벨에서 해야한다. (옆으로)

 

 

 

 

✔️ 2단계 초기화

클래스가 상속한 경우 부모 클래스의 프로퍼티와 자식 클래스의 프로퍼티를 한 번에 초기화하는 방법이다.

원래 Swift에서 클래스는 2단계를 통해서 초기화된다.

 

 

 

🔍 첫번째 단계

  • designated initalizer 혹은 convenience initalizer를 호출한다.
  • 인스턴스에 대한 Memory allocation을 수행한다. 다만 이 때는 stored properties에 들어가는 데이터의 크기를 모르기 때문에 정확한 메모리 크기가 설정되어 있지 않는다.
  • designated initalizer가 모든 stored properties가 설정되었는지 체크 후 프로퍼티들에 대한 메모리 초기화를 수행한다.
  • 이제 서브클래스의 작업을 마치고 슈퍼클래스에서 위의 과정과 동일한 작업을 수행한다.
  • 계층 구조 최상단 슈퍼클래스가 모든 프로퍼티들이 값이 있는지 확인 후 초기화 작업이 완료된다.

 

 

🔍 두번째 단계

  • 첫번째 단계를 마치면 self에 접근할 수 있게 되고, 최상단 슈퍼클래스는 프로퍼티 값을 변경할 기회를 얻게 된다.
  • 값 변경의 기회는 계층 구조 최상단 슈퍼클래스부터 마지막 서브클래스의 순서로 주어진다.
  • 현재 클래스에서 이니셜라이저를 호출한 것이 convenience initalizer라면, designated initalizer부터 우선적으로 self의 값을 변경할 기회를 얻게 된다. 이 작업을 마치면 최종적으로 convenience initalizer 값을 변경할 기회를 얻게 된다.

 

위와 같은 단계를 참고하여 예제 코드를 작성해보았다.
(숫자를 따라가보자.)

class A {
    var name: String
    init(name: String) { // 1 , 9
        self.name = name
    }
}

class B: A {
    var a: Int
    var b: Int
    
    init(n: String, a: Int, b: Int) { // 2 , 8
        self.a = a
        self.b = b
        super.init(name: n)
    }
    
    convenience init(c: Int) { // 5
        self.init(n: "B", a: c, b: c)
    }
}

class C: B {
    convenience init(d: Int) { // 3 , 7
        self.init(n: "C", a: d, b: d)
    }
    
    convenience init() { // 4 , 6
        self.init(n: "C_1", a: 0, b: 0)
    }
}

 

 

 

 

✔️ 2단계 초기화를 위한 safery-check

 

🔍  Rule 1

designated initializer에서는 현재 클래스에서 정의한 stored properties의 값이 delegate up되기 이전에 모두 초깃값을 가지고 있어야 한다. 메모리에서 객체는 모든 저장된 프로퍼티가 초기 상태를 가져야만 완전히 초기화된 것으로 간주하기 때문이다

🖐🏻  핵심

super.init()을 호출할 경우 현재 클래스의 초기화를 중단한 후 메모리에 올려진다. 메모리에 올려질 때 초기화가 되어있어야만 에러가 없다.

 

 

 

🔍  Rule 2

designated initializer는 상속받은 property의 값을 넣기 전에 superClass의 initializer를 호출해야 한다.(delegate up)

super.init()을 호출하지 않으면 아직 superclass가 초기화 되지 않은 상태이므로 상속된 값을 사용할 수 없다.

class A {
    let b: Int
    
    init() {
        b = 2 // 상속하기 전에 superclass 프로퍼티 모두 초기화
    }
}

class B: A {
    let a: Int
    
    init(i: Int) {
        a = 1 // super.init()호출 하기전에 자식 클래스 B의 프로퍼티 초기화
        super.init() // 상속된 값을 사용하기 전에 superclass 초기화
        print(b)
    }
}

 

 

 

🔍  Rule 3

Convenience initalizer는 반드시 어떤 프로퍼티를 할당하기 전에 initializer delegation을 수행해야 한다.

Convenience initalizer 후에 다른 초기자로 위임을 넘기게 되면 Convenience initalizer한 값이 묻히게 된다.

class A {
    var b: Int = 2

    convenience init(c: Int) {
        self.init() // 어떤 프로퍼티를 할당하기 전에 다른 초기자로 위임.
        b = c // success
    }
}

 

 

 

🔍  Rule 4

1단계를 하지 않으면 self로 프로퍼티나 메서드를 참조할 수 없다.

class A {
    var b: Int = 2

    convenience init(c: Int) {
        self.b = 3 // error
        self.init()
    }
}

 

 

 

✔️ 이니셜라이저의 상속

Swift는 기본적으로 자식 클래스에서 부모 클래스의 이니셜라이저를 상속하지 않는다.

무분별하게 상속이 되면 복잡도가 높아져서 자식 클래스를 잘못 초기화하는 상황이 생기기 때문이다.

그렇기 때문에 특정 조건을 만족할 때에만 이니셜라이저를 상속한다.

 

 

 

🔍  상속하기 위한 조건

자식 클래스가 Designated initalizer를 정의하지 않은 경우 Designated initalizer를 상속받는다.

자식 클래스가 부모 클래스의 Designated initalizer를 모두 구현(상속 또는 오버라이드)한 경우 Convenience initalizer를 상속받는다.

 

 

 

 

 

🔍  예제 코드

class Food {
    var name: String
    init(name: String) { // Food의 Designated initalizer
        self.name = name // Food의 모든 프로퍼티 초기화
    }
    convenience init() {
        self.init(name: "[Unnamed]") // Designated initalizer 호출하여 매개변수에 기본값 전달
    }
}

let pasta = Food(name: "파스타")
let null = Food()
print(pasta.name, null.name) // 파스타 [Unnamed]

class RecipeIngredient: Food {
    var quantity: Int
    init(name: String, quantity: Int) { // RecipeIngredient의 Designated initalizer
        self.quantity = quantity
        super.init(name: name) // 부모 class의 이니셜라이저 호출하여 name에 값 전달
    }
    // 부모 클래스의 이니셜라이저와 겹치기 때문에 override 사용하여 재정의.
    // 매개변수명과 Type으로 구분하기 때문이다.
    override convenience init(name: String) {
        self.init(name: name, quantity: 1)
    }
}

// 부모 클래스의 모든 이니셜라이저를 구현한 경우 부모 클래스가 가진 convenience initalizer를 상속 받는다.
let nullTwo = RecipeIngredient()
let spam = RecipeIngredient(name: "스팸")
let apple = RecipeIngredient(name: "사과", quantity: 3)
print(nullTwo.name, spam.name, apple.name) // [Unnamed] 스팸 사과
print(nullTwo.quantity, spam.quantity, apple.quantity) // 1 1 3

// 새로 추가된 모든 프로퍼티가 기본값을 가지고 있으며 별도의 이니셜라이저를 적용하지 않았으므로
// 부모클래스의 모든 프로퍼티를 자동으로 상속한다.
class ShoppingListItem: RecipeIngredient {
    var purchased = false
    var description: String {
        var output = "\(quantity) x \(name)"
        output += purchased ? " ✔" : " ✘"
        return output
    }
}

let coke = ShoppingListItem(name: "콜라", quantity: 100)
coke.purchased = true
print(coke.description) // 100 x 콜라 ✔

 

 

✔️  Reference

 

Initialization — The Swift Programming Language (Swift 5.5)

Initialization Initialization is the process of preparing an instance of a class, structure, or enumeration for use. This process involves setting an initial value for each stored property on that instance and performing any other setup or initialization t

docs.swift.org

 

반응형
Comments