본문 바로가기

Swift

Initialization - 상속과 초기화

728x90

클래스 상속과 초기화

모든 클래스의 저장 프로퍼티와 부모 클래스로부터 상속받은 모든 프로퍼티는 초기화 단계에서 반드시 초기값이 할당되어야 합니다. Swift에서는 클래스 타입에서 모든 프로퍼티가 초기 값 할당을 보장하기 위해 2가지 방법을 지원합니다.

 

지정 초기자와 편리한 초기자

✔️ 지정 초기자는 클래스의 주초기자입니다. 지정 초기자는 클래스의 모든 프로퍼티를 초기화합니다. 클래스 타입은 반드시 1개 이상의 지정 초기자가 있어야 합니다. 

✔️ 편리한 초기자는 초기화 단계에서 미리 지정된 값을 사용해서 최소한의 입력으로 초기화를 할 수 있도록 도와줍니다. 편리한 초기자 내에서 반드시 지정 초기자가 호출되어야 합니다. 

 

지정 초기자의 문법

지정 초기자의 문법은 값 타입 초기자와 같습니다.

init(parameters) {
    statements
}

 

편리한 초기자의 문법

편리한 초기자는 기본 초기자와 문법이 같지만 init 앞에 convenience 키워드를 붙입니다.

convenience init(parameters) {
    statements
}

 

클래스  타입을 위한 이니셜라이저 위임 

지정 초기자와 편리한 초기자 사이의 관계를 단순하게 하기 위해 Swift는 3가지 규칙을 따릅니다. ⬇️

 

  1. 지정 초기자는 반드시 직계 부모 클래스의 지정 초기자를 호출해야 합니다.
  2. 편리한 초기자는 반드시 같은 클래스의 다른 초기자를 호출해야 합니다.
  3. 편리한 초기자는 궁극적으로 지정 초기자를 호출해야 합니다. 

즉, 지정 초기자는 반드시 위임을 superclass로 해야 하고 편리한 초기자는 반드시 같은 레벨에서 위임해야 합니다.

 

위의 그림을 통해서 규칙을 보다 잘 이해할 수 있습니다.

그림을 보게 되면, subclass의 편리한 초기자는 같은 레벨에서 다른 지정 초기자를 호출하고 지정 초기자는 초기화를 상위 클래스에 위임하여 상위 클래스의 지정 초기자가 호출된 것을 확인할 수 있습니다.

 

💬 초기자 호출의 모양이 반드시 위와 같을 필요는 없습니다. 다만, 위에서 언급한 규칙을 지켜야 합니다. (규칙만 지켜진다면, 모양은 달라도 상관없습니다.)

 

아래의 그림은 보다 복잡한 초기화 위임의 형태입니다.

2단계 초기화

Swift에서 클래스 초기화는 2단계로 진행됩니다. 

✔️ 첫 번째 단계에서는 각 저장된 프로퍼티가 초기값으로 초기화 됩니다. 모든 저장된 프로퍼티의 상태가 결정이 되면 두번째 단계가 시작됩니다.

✔️ 두번째 단계에서는 새로운 인스턴스의 사용이 준비되었다고 알려주기 전에 저장된 프로퍼티를 커스터마이징 하는 단계입니다.

 

💬 Swift의 2단계 초기화는 Objective-C에서의 초기화와 유사합니다. 차이점은 첫번째 단계입니다. Objective-C에서는 모든 프로퍼티에 0 혹은 nill 값을 할당합니다. Swift의 경우, 보다 유연하게 커스텀한 초기 값을 할당할 수 있습니다. 그리고 0과 nil이 잘못된 초기값으로 지정된 경우 대처할 수 있습니다.

 

Swift의 컴파일러는 2단계 초기화가 에러 없이 끝나는 것을 보장하기 위해 4단계 안전 확인을 합니다.

2단계 초기화 [단계 1]
2단계 초기화 [단계 2]

 

1️⃣ 안전 확인 1단계

지정 초기자는 클래스 안에서 초기화를 superclass의 초기자에게 위임하기 전에 모든 프로퍼티를 초기화해야 합니다. 메모리에서 객체는 모든 저장된 프로퍼티가 초기 상태를 갖고 있어야만 완전히 초기화 된 것으로 간주하기 때문에 지정 초기자는 반드시 다른 초기자로 넘기기 전에 소유하고 있는 모든 프로퍼티를 초기화 해야 합니다.

 

2️⃣ 안전 확인 2단계

지정 초기자는 반드시 상속된 값을 할당하기 전에 superclass의 초기자로 위임을 넘겨야 합니다. 그렇지 않으며 상속된 값이 superclas의 초기자에 의해 덮어 쓰이게 됩니다.

 

3️⃣ 안전 확인 3단계

편리한 초기자는 반드시 어떤 프로퍼티를 할당하기 전에 다른 초기자로 위임을 넘겨야 합니다. 그렇지 않으면 편리한 초기자에 의해 할당된 값을 다른 클래스의 지정 초기자에 의해 덮어 쓰여지게 됩니다.

 

4️⃣ 안전 확인 4단계

이니셜라이저는 초기화의 1단계가 끝나기 전에는 self의 값을 참조하거나 어떤 인스턴스 프로퍼티, 메서드 등을 호출하거나 읽을 수 없습니다.

 

이니셜라이저의 상속과 오버라이딩

Swift에서는 기본적으로 subclass에서 superclass의 이니셜라이저를 상속하지 않습니다. 왜냐하면, superclass의 이니셜라이저가 무분별하게 상속되어 복잡해져 subclass에서 이들이 잘못 초기화되는 것을 막기 위해서입니다. 

 

💬 superclass의 이니셜라이저는 안전하고 적당한 특정 환경에서 상속됩니다.

 

만약 클래스에서 모든 프로퍼티의 초기 값이 지정되어 있고 아무런 커스텀 초기자를 선언하지 않았다면 기본 초기자를 사용할 수 있습니다.

superclass의 초기자를 오버라이드 하기 위해서는 subclass에서 그 초기자에 override 키워드를 붙이고 재정의를 합니다.

 

아래의 코드는 클래스를 생성하고 그것의 subclass에서 초기자를 오버라이드 하여 사용하는 예제입니다.

먼저,  인스턴스를 생성하고 초기 값이 지정한 값으로 나오는지 확인합니다. 

class Vehicle {
    var numberOfWheels = 0
    var description: String {
        return "\(numberOfWheels) wheel(s)"
    }
}

let vehicle = Vehicle()
print("Vehicle: \(vehicle.description)") // Vehicle: 0 wheel(s)

 

그리고 subclass에서 superclass의 초기자를 오버라이드 합니다.

class Bicycle: Vehicle {
    override init() {
        super.init()
        numberOfWheels = 2
    }
}

인스턴스를 생성하면, 초기 값이 변한 것을 확인할 수 있습니다. 

let bicycle = Bicycle()
print("Bicycle: \(bicycle.description)") // Bicycle: 2 wheel(s)

 

💬 subclass의 초기자에서 var은 변경 가능하지만, let은 변경할 수 없습니다. 

 

자동 초기자 인스턴스

위에서 언급한 것과 같이 subclass는 superclass의 초기자를 기본적으로 상속받지 않습니다. 하지만 특정 상황에서는 자동으로 상속을 받습니다. (사실 많은 상황에서 직접 초기자를 오버라이드 할 필요는 없습니다.)

 

서브 클래스에서 새로 추가한 모든 프로퍼티에 기본 값을 제공하면 다음 두 가지 규칙이 적용됩니다.

1️⃣ 서브 클래스가 지정 초기자를 정의하지 않으면 자동으로 수퍼 클래스의 모든 지정 초기자가 상속됩니다.

2️⃣ 서브 클래스가 수퍼 클래스의 지정 초기자를 모두 구현한 경우, 자동으로 수퍼 클래스의 편리한 초기자를 추가합니다. 

 

💬 규칙 2에 따라서 서브 클래스는 수퍼 클래스의 지정 클래스를 서브 클래스의 편리한 초기자로 구현이 가능합니다. 

 

지정 초기자와 편리한 초기자의 사용

아래의 코드는 지정 초기자/편리한 초기자/자동 초기자의 상속 시 예제입니다.

class Food {
    var name: String
    
    init(name: String) {
        self.name = name
    }
    
    convenience init() {
        self.init(name: "[Unnamed]")
    }
}

위의 코드는 편리한 초기자에서 지정 초기자가 호출되는 형태입니다. Food 클래스는 모든 프로퍼티의 기본 값이 있지 않으므로 membersiw 초기자를 갖지 않습니다.

 

let namedMeat = Food(name: "Bacon")
// namedMeat's name is "Bacon"

위의 코드는 지정 초기자를 이용해서 Food 인스턴스의 name을 "Bacon"으로 초기화하고 생성하는 예제입니다.

 

let mysteryMeat = Food()
// mysteryMeat's name is "[Unnamed]"

그리고 위의 코드는 편리한 초기자를 이용해 인스턴스를 생성한 예제입니다.

 

다음 코드는 Food 클래스를 subclassing 해서 생성한 RecipeIngredient 클래스에서 수퍼클래스의 편리한 초기자를 오버라이딩해서 사용한 예제입니다.

class RecipeIngredient: Food {
    var quantity: Int
    
    init(name: String, quantity: Int) {
        self.quantity = quantity
        super.init(name: name)
    }
    
    // Food의 편리한 초기자를 오버라이딩 
    override convenience init(name: String) {
        self.init(name: name, quantity: 1)
    }
}

RecipeIngredient에서 초기자가 사용되는 구조를 표현하면 다음 그림과 같습니다. ⬇️

 

이렇게 생성한 RecipeIngredient 클래스는 다음 3가지 형태의 초기자를 이용해서 인스턴스를 생성할 수 있습니다.

let oneMysteryItem = RecipeIngredient()
let oneBacon = RecipeIngredient(name: "Bacon")
let sixEggs = RecipeIngredient(name: "Eggs", quantity: 6)

 

그리고 이 RecipeIngredient를 상속받아 생성한 ShoppingListItem 클래스에서의 초기자의 경우 아래와 같습니다.

ShoppingListItem은 bool 형의 purchased 프로퍼티와 description이라는 계산된 프로퍼티 2가지를 새로 생성합니다.

class ShoppingListItem: RecipeIngredient {
    var purchased = false
    
    var description: String {
        var output = "\(quantity) x \(name)"
        output += purchased ? " ✔" : " ✘"
        return output
    }
}

💬 ShoppingListItem 클래스에서 purchased 프로퍼티 값은 언제나 구매하지 않음(purchased = false)을 시작 값으로 사용하기 때문에 이 값을 변경하는 초기자를 제공하지 않습니다.

 

ShoppingListItem 클래스는 새로 생성한 모든 프로퍼티에 대해 기본 값을 제공하고 새로운 초기자를 정의하지 않았기 때문에 자동으로 수퍼클래스의 모든 지정초기자와 편리한 초기자를 상속받습니다. ShoppingListItem 클래스에서 초기자가 호출되는 구조를 살펴보면 다음과 같습니다.

결국, ShoppingListItem 인스턴스의 경우 3가지 초기자를 사용해서 생성할 수 있습니다.

 

var breakfastList = [
    ShoppingListItem(),
    ShoppingListItem(name: "Bacon"),
    ShoppingListItem(name: "Eggs", quantity: 6),
]

breakfastList[0].name = "Orange juice"
breakfastList[0].purchased = true

for item in breakfastList {
    print(item.description)
}
// 1 x Orange juice ✔
// 1 x Bacon ✘
// 6 x Eggs ✘

'Swift' 카테고리의 다른 글

Initialization - 구조체/클래스 초기화  (0) 2022.04.21
Initialization - 그 외  (0) 2022.04.20
Initialization - 무엇인가  (0) 2022.04.20
Hashable  (0) 2022.04.14
MVVM+RxSwift  (0) 2022.04.07