티스토리 뷰

728x90

클래스 상속과 초기화 (Class Inheritance and Initialization)

 

모든 클래스의 저장 프로퍼티와 상위클래스로부터 상속받은 모든 프로퍼티는 초기화 단계에서 반드시 초기값이 할당 되어야 합니다.

Swift에서는 클래스 타입에서 모든 프로퍼티가 초기값을 갖는걸 보장하기 위해 2가지 방법을 지원합니다.

 

지정 초기자편의 초기자 (Designated Initializers and Convenience Initializers)

 

지정 초기자는 클래스의 주요 초기자입니다.

클래스의 모든 프로퍼티를 초기화 하며, 클래스 타입은 반드시 한개 이상의 지정 초기자가 있어야 합니다.

 

편의 초기자는 초기화 단계에서 미리 지정된 값을 사용해 초기화를 할 수 있도록 해주는 초기자 입니다.

편의 초기자 내에서 반드시 지정 초기자가 호출되어야 합니다.

 

문법

 

지정 초기자 문법은 값타입 초기자와 똑같은 방법으로 쓰입니다.

init(parameters) {
  statements
}

 

편의 초기자 문법은 앞에 init 앞에 convenience 키워드를 붙여주면 됩니다.

convenience init(parameters) {
  statements
}

 

클래스 타입을 위한 상속과 초기화 (Class Inheritance and Initialization)

 

designated initializer(지정 초기자) 와 convenience initializer (편의 초기자) 사이의 관계를 단순하게 하기 위해, 

Swift는 이니셜라이저 간의 위임 호출에 대해 3가지 규칙들을 적용합니다. 

 

Rule 1

  지정 초기자는 반드시 직계 상위클래스의 지정 초기자를 호출해야 합니다.

 

Rule 2

  편의 초기자는 반드시 같은 클래스에있는 다른 초기자를 호출해야 합니다.

 

Rule 3

  편의 초기자는 궁극적으로 지정 초기자를 호출해야 합니다.

 

즉,

  • 지정 초기자는 항상 위임을 상위클래스로 해야하고,

  • 편의 초기자는 항상 위임을 같은 클래스에서 해야합니다.

 

아래 그림을 통해 보자면,

 

Convenience (편의 초기자)를 먼저 봅시다.

같은 클래스 안에서만 지정 초기자, 편의 초기자를 호출하는걸 볼 수 있습니다. 

 

Designated (지정 초기자)를 보면,

상위 클래스만 호출하거나, 호출하지 않는걸 볼 수 있습니다. (최상위 클래스의 지정 초기자일 경우 호출x)

 

조금 더 복잡한 초기화 위임 형태를 보겠습니다.

 

2단계 초기화 (Two-Phase Initialization)

 

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

 

1단계

  - 각 저장된 프로퍼티는 초기값으로 초기화 됩니다.

  - 모든 저장된 프로퍼티의 초기상태가 결정되면 2단계가 시작됩니다.

 

2단계

  - 새로운 인스턴스의 사용이 준비됐다고 알려주기 전에 저장된 프로퍼티를 커스터마이징하는 단계 입니다.

 

NOTE

Swift의 2단계 초기화과정은 Objective-C의 초기화와 유사합니다. 
주요 차이점은 1단계 부분 입니다.
Objective-C에서 모든 프로퍼티에 0 또는 null 값을 할당하게 되지만,
Swift에서의 초기화 과정은 더 유연합니다.
커스텀한 초기값을 할당하며, 0과 nil이 유효하지 않은 기본값일 경우 대처할 수 있습니다.

 

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

 

안전확인 1단계

  지정 초기자는 클래스 안에서 상위클래스의 초기자에게 위임하기 전에 모든 프로퍼티를 초기화 해야 합니다.

 

안전확인 2단계

  지정 초기자는 상위클래스 초기자에게 상속된 프로퍼티의 값이 할당되기 전에 위임해야 합니다.

  그렇지 않으면 상속된 값이 상위 클래스 초기자에 의해 덮어 쓰여지게 됩니다.

 

안전확인 3단계

  편의 초기자는 어떠한 프로퍼티를 다른 초기자에게 값을 할당하기 전에 위임을 넘겨야 합니다.

  그렇지 않으면 편의 초기자에 의해 할당된 값을 다른 클래스의 지정 초기자에 의해 덮어 쓰여지게 됩니다.

 

안전확인 4단계

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

 

그림으로 초기화 흐름 (flow) 보기 

아래는 위에 설명한 초기화 상황을 그림으로 나타낸 것입니다.

 

1단계 

 

설명...

더보기

  하위 클래스에서 편의 초기자를 호출하면서 초기화가 시작됩니다.

  편의 초기자는 어떠한 프로퍼티들도 아직은 수정할 수 없으며, 같은 클래스 안에 있는 지정 초기자에게 위임 합니다.

  위 안전검사 1단계에 따라, 지정 초기자는 모든 하위클래스의 프로퍼티들이 값을 갖고 있다는걸 보장하며, 

  후에 상위클래스의 지정 초기자를 호출하여 상위클래스로 넘어갑니다.

 

  상위클래스의 지정 초기자는 모든 상위클래스 프로퍼티들이 값을 갖고 있다는걸 보장합니다.

  최상위 클래스이므로 더이상 위임이나 초기화가 필요하지 않습니다.

 

  상위클래스의 모든 프로퍼티들이 초기값을 갖자마자, 메모리는 전체적으로 초기화되며 1단계가 완료 됩니다.

 

2단계

 

예) 설명..

더보기

상위클래스의 지정 초기자는 이제 인스턴스를 추가하여 커스터마이징 할 수 있습니다.

 

상위클래스의 지정 초기자가 종료되면, 하위클래스의 지정 초기자는 추가 커스텀을 할 수 있습니다.

 

마지막으로, 하위클래스의 지정 초기자가 종료되면, 원래 호출된 편의 초기자는 추가 커스텀을 할 수 있습니다.

 


이니셜라이저의 상속과 오버라이딩 (Initializer Inheritance and Overriding)

 

Objective-C와는 다르게 Swift에선 하위클래스에서 상위클래스의 초기자를 상속하지 않습니다.

why? - 상위클래스의 초기자가 무분별하게 상속될 경우 하위클래스에서 잘못 초기화 되는 것을 막기 위해

 

만약 클래스에서 모든 프로퍼티의 초기값이 지정되어 있고,

커스텀 초기자를 선언하지 않았다면 기본 초기자 Init()을 사용할 수 있습니다.

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

// 초기값이 위에 지정한 대로 나오는지 확인
let vehicle = Vehicle()
print("Vehicle: \(vehicle.description)")
// Vehicle: 0 wheel(s)

 

상위클래스의 초기자를 오버라이드 하기 위해서는

하위클래스의 초기자에 override 키워드를 붙여주면 재정의 할 수 있습니다.

 

// Bicycle(하위클래스)에서 Vehicle(상위클래스)의 초기자를 오버라이드 합니다.
class Bicycle: Vehicle {
    override init() {
        super.init()
        numberOfWheels = 2
    }
}

// 인스턴스 생성 후 초기값이 변경된것 확인
let bicycle = Bicycle()
print("Bicycle: \(bicycle.description)")
// Bicycle: 2 wheel(s)

 

 

NOTE

 

하위클래스 초기자에서 var는 변경할 수 있지만 let은 변경할 수 없습니다.

 

 

자동 초기자 상속 (Automatic Initializer Inheritance)

 

하위클래스는 상위클래스 초기자를 상속하지 않지만, 특정 상황에서 자동으로 상속 받습니다.

실제로, 많은 상황에서 직접 초기자를 오버라이드할 필요는 없습니다. 

 

(가정) 하위클래스에 새로 추가한 모든 프로퍼티들에 기본값을 제공하면 아래와 같은 2가지 규칙이 적용 됩니다.

 

규칙 1

  하위클래스에  지정 초기자들을  정의하지 않으면, 자동으로 상위클래스의 지정 초기자들에 상속합니다.

 

규칙 2

 

  하위클래스가 모든 상위클래스 지정 초기자들을 모두 구현한 경우,

  자동으로 모든 상위클래스의 편의 초기자에 상속합니다. 

 

NOTE

규칙2에 따라 하위클래스는 상위클래스의 지정 초기자를 하위클래스의 편의 초기자로 구현 가능합니다.

 

 

 

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

 

아래는 지정 초기자와 편의 초기자, 자동 초기자의 상속 예제 입니다.

class Food {
    var name: String
    init(name: String) {  // 지정 초기자 (Designated initializer)
        self.name = name
    }
    convenience init() {  // 편의 초기자 (Convenience initializer)
        self.init(name: "[Unnamed]")
    }
}

편의 초기자 convenience init() 에서 지정 초기자 init(name: String)이 호출되는 형태입니다.

 

 

Food 클래스는 프로퍼티에 기본값이 없으므로 memberwise 초기자를 갖지 않습니다.

 

아래는 지정 초기자와 편의 초기자로 값을 변경하는 예제 입니다.

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

// 지정 초기자에 접근해 name의 초기값을 "Bacon"으로 변경 합니다.
let namedMeat = Food(name: "Bacon") // 지정 초기자 사용
// namedMeat's name is "Bacon"


// 편의 초기자에 의해 "[Unnamed]" 값을 갖게 됩니다.
let mysteryMeat = Food()
// mysteryMeat's name is "[Unnamed]"

 

다음 코드는 Food 클래스를 상속받는 RecipeIngredient 클래스에서 Food 클래스의 편의 초기자를 오버라이딩 하여 사용한 예제 입니다.

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

class RecipeIngredient: Food {
    var quantity: Int
    
    init(name: String, quantity: Int) {
        self.quantity = quantity
        super.init(name: name)
    }
    
    override convenience init(name: String) {
        self.init(name: name, quantity: 1)
    }
}

 

위 코드의 흐름을 그림으로 표현 하자면 아래와 같습니다.

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

let oneMysteryItem = RecipeIngredient()  // 1번
let oneBacon = RecipeIngredient(name: "Bacon")  // 2번
let sixEggs = RecipeIngredient(name: "Eggs", quantity: 6)  // 3번

1번 설명

더보기
let oneMysteryItem = RecipeIngredient()

 

넘겨주는 인자가 없으므로, 코드 흐름은 이런 형태가 되겠네요.

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

class RecipeIngredient: Food {
    var quantity: Int
    
    init(name: String, quantity: Int) {       // 2 - 편의 초기자에서 받아온 값 적용 
        self.quantity = quantity              // quantity = 1
        super.init(name: name)                // name = "[Unnamed]"
    }
    
    override convenience init(name: String) {  // 1 - Food클래스의 편의 초기자 name: "[Unnamed]" 적용
        self.init(name: name, quantity: 1)
    }
}

 

oneMysteryItem의 값은 아래와 같이 초기화 됩니다.

2번 설명

더보기
let oneBacon = RecipeIngredient(name: "Bacon")

name: 의 인자로 "Bacon"이 들어오는걸 볼 수 있습니다.

class Food {
    var name: String
    init(name: "Bacon") {                       // 3 - 하위클래스에서 전달 받은 name 값 "Bacon"으로 적용
        self.name = "Bacon"
    }
    
    convenience init() {
      self.init(name: "[Unnamed]")
    }
}

class RecipeIngredient: Food {
	var quantity: Int
    init(name: "Bacon" quantity: 1) {            // 2 - 편의 초기자에서 받은 값 적용
    	self.quantity = 1                        
        super.init(name: "Bacon")                
    }
    override convenience init(name: "Bacon") {   // 1 - 편의 초기자 -> 지정 초기자
      self.init(name: "Bacon", quantity: 1)      
    }
}

 

oneBacon의 값은 아래와 같이 초기화 됩니다.

3번 설명

더보기
let sixEggs = RecipeIngredient(name: "Eggs", quantity: 6)

 name: 의 인자로 "Eggs"가, quantity: 의 인자로 6 이 들어오는걸 볼 수 있습니다.

class Food {
    var name: String
    init(name: "Eggs") {                       // 3 - 하위클래스에서 전달 받은 name 값 "Eggs"으로 적용
        self.name = "Eggs"
    }
    
    convenience init() {
      self.init(name: "Eggs")
    }
}

class RecipeIngredient: Food {
	var quantity: Int
    init(name: "Eggs" quantity: 6) {            // 2 - 편의 초기자에서 받은 quality 값 6 적용, Food클래스의 지정 초기자 name 값 "Eggs" 적용 
    	self.quantity = 6                        
        super.init(name: "Eggs")                
    }
    override convenience init(name: "Eggs", quantity: 6) {   // 1 - 편의 초기자 -> 지정 초기자
      self.init(name: "Eggs", quantity: 6)      
    }
}

 

sixEggs 의 값은 아래와 같이 초기화 됩니다.

 

 

마지막으로 RecipeIngredient 클래스를 상속받은 ShoppingListItem 클래스의 초기자를 알아보겠습니다.

ShoppingListItem은 purchased 라는 Bool 프로퍼티, description이라는 연산 프로퍼티 2가지를 새로 생성합니다.

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

 

ShoppingListItem 클래스에서 purchased 프로퍼티 값은 항상 false 이므로, 이 값에 대한 초기자를 제공하지 않습니다.

ShoppingListItem 클래스는 새로 생성한 모든 프로퍼티에 대해 기본 값을 제공하고,

새로운 초기자를 정의하지 않았기 때문에 자동으로 상위클래스의 모든 지정 초기자와 편의 초기자를 상속 받습니다. 

 

 

이제 이 모든 3개의 초기자를 통해 ShoppingListItem 인스턴스를 생성할 수 있습니다.

 

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

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 ✘

정리내용이 꽤나 많은데 

조금씩 정리하면서 이해가 간것 같습니다.

 

조만간 지정 초기자, 편의 초기자는 좀더 간략하게

정리해서 블로깅 하겠습니다!!

728x90