개발자의 삽질
[Swift] Initialization - 2편 (Class Inheritance & Initialization) 본문
[Swift] Initialization - 2편 (Class Inheritance & Initialization)
uniqueimaginate 2022. 1. 27. 23:30https://docs.swift.org/swift-book/LanguageGuide/Initialization.html
1편 보기
2022.01.24 - [Swift] - [Swift] Initialization - 1편
계속해서 Initialization에 대해 알아보자
Class Inheritance and Initialization
클래스가 가진 모든 저장 프로퍼티(부모로부터 상속받은 프로퍼티 포함해서)는 초기화 단계에서 반드시 초기값을 할당해야 한다!
Swift는 모든 저장 프로퍼티가 초기값을 갖게 하기 위해서 designated initializer와 convenience initializer를 제공한다.
Designated Initializers and Convenience Initializers
Designated initializer는 클래스의 주 생성자이다. 주 생성자는 클래스가 만든 모든 프로퍼티에 값을 초기화 수 있고 부모 클래스의 적절한 생성자를 호출해서 부모 단계까지 초기화를 할 수 있게 해 준다.
클래스는 적은 수의 생성자를 가지며 대개 한 개를 갖게 된다.
모든 클래스는 적어도 한 개의 designated initializer를 가져야만 한다
Convenience initializer는 클래스의 보조 생성자이다.
Designated initializer의 파라미터를 기본값으로 설정하여 Convenience initializer와 동일한 클래스에서 Designated initializer을 호출하도록 Convenience initializer를 정의할 수 있다.
-> 쉽게 말하면, 그냥 Designated initializer의 파라미터 중에서 기본값을 주고 싶은 게 있다면 값을 미리 넣어주는 것이다!
class Polygon {
var points: [CGPoint]
init(points: [CGPoint]) {
self.points = points
}
}
convenience init(squareWithLength length: CGFloat) {
let points = [
CGPoint(x: 0, y: 0),
CGPoint(x: length, y: 0),
CGPoint(x: length, y: length),
CGPoint(x: 0, y: length),
]
self.init(points: points)
}
Syntax for Designated and Convenience Initializers
Designated initializer
Convenience initializer
Initializer Delegation for Class Types
Designated, Convenience initializer 간의 관계를 명료하게 하기 위해서 Swift는 다음 3개의 규칙을 따르고 있다.
- Designated initializer는 바로 위 부모 클래스의 Designated initializer를 반드시 호출해야 한다.
- Convenience initializer는 같은 클래스에 있는 다른 생성자를 반드시 호출해야 한다.
- Convenience initializer는 궁극적으로 Designated initializer를 호출해야 한다.
간단하게 말하자면
Designated initializer는 반드시 위로 위임하고
Convenience initializer는 반드시 옆으로 위임한다.
Two-Phase Initialization
Swift는 2단계의 과정을 거쳐 클래스 초기화를 진행한다.
1 단계 : 모든 저장 프로퍼티에 초기 값을 할당한다.
2 단계: 1 단계를 마치면, 인스턴스를 사용할 준비가 되었기 때문에 클래스는 저장 프로퍼티를 수정할 수 있다.
이러한 2단계를 통해서 더욱더 안전한 초기화를 진행할 수 있다.
값이 초기화되기 전에 저장 프로퍼티에 대한 접근을 막을 수 있고, 다른 생성자를 통해서 저장 프로퍼티의 값이 변경되는 것도 막을 수 있게 된다.
Safety Check 1
Designated initializer는 클래스에 있는 저장 프로퍼티가 모두 초기화된 후에야 부모 클래스의 생성자를 호출해야 한다.
Safety Check 2
Designated initializer는 상속받은 프로퍼티에 대해 값을 할당하기 전에 반드시 부모 클래스의 생성자를 호출해야 한다.
만약 그러지 않는다면, designated initializer가 할당한 값을 부모클래스의 생성자가 덮어쓰게 될 것이다.
Safety Check 3
Convenience initializer는 클래스에 있는 저장 프로퍼티에 값을 할당하기 전에 반드시 클래스 내의 다른 생성자를 먼저 호출해야 한다.
만약 그러지 않는다면, convenience initializer가 할당한 값을 다른 생성자가 덮어 쓰게 될 것이다.
Safety Check 4
생성자는 1단계 초기화가 완료되기 전까지 인스턴스 메서드를 호출할 수 없고, 인스턴스 프로퍼티를 읽을 수 없으며, self를 통해 자기 자신을 참조할 수 없다
Phase 1
- Designated 또는 Convenience 생성자가 호출된다.
- 클래스의 새로운 인스턴스가 메모리에 할당된다. 그러나 아직 초기화되지는 않았다.
- Designated initializer가 모든 저장 프로퍼티에 값을 가졌는지 확인한다. 이제 메모리에 담겨있는 모든 저장 프로퍼티는 초기화되었다.
- Designated initializer는 이제 부모 클래스의 생성자를 호출해 위에서 한 일들을 동일하게 수행한다.
- 이러한 과정은 상속 체인의 가장 위까지 계속해서 진행된다.
- 상속 체인에서 가장 높은 체인까지 도달했다면, 이 인스턴스의 메모리는 완전히 초기화되었으며 이로써 1단계가 완료되었다.
Phase 2
- 가장 높은 단계에서 다리 내려오면서 일을 수행하게 된다. 각각의 Designated initializer는 인스턴스를 수정할 수 있게 된다. Designated initializer는 이제 self에 접근할 수 있으며, 인스턴스가 가진 프로퍼티를 수정하고 메서드를 호출할 수 있다.
- 최종적으로, convenience initializer이 self를 이용해서 인스턴스를 수정할 수 있게 된다.
Initializer Inheritance and Overriding
Objective-C와는 다르게, Swift 자식 클래스는 부모 클래스의 생성자를 기본적으로 상속받지 않는다.
만약 자식 클래스가 부모 클래스의 생성자를 상속받아 사용하고 싶다면 override 키워드를 이용하면 된다.
반대로 자식 클래스가 부모 클래스의 Convenience initializer와 같은 Convenience initializer를 쓸 때는 override 키워드를 사용하지 않아도 된다. 왜냐하면, 자식 클래스는 부모 클래스의 Convenience initializer를 직접 호출 할 수 없기 때문이다!! (1편에서 다루었다!!)
class Vehicle {
var numberOfWheels = 0
var description: String {
return "\(numberOfWheels) wheel(s)"
}
}
let vehicle = Vehicle()
print("Vehicle: \(vehicle.description)")
// Vehicle: 0 wheel(s)
class Bicycle: Vehicle {
override init() {
super.init()
numberOfWheels = 2
}
}
let bicycle = Bicycle()
print("Bicycle: \(bicycle.description)")
// Bicycle: 2 wheel(s)
만약 자식 클래스의 생성자가 초기화 2단계에서 수정을 하지 않고, 부모 클래스의 생성자에 파라미터가 없다면 super.init()을 생략할 수 있다.
class Hoverboard: Vehicle {
var color: String
init(color: String) {
self.color = color
// super.init() implicitly called here
}
override var description: String {
return "\(super.description) in a beautiful \(color)"
}
}
let hoverboard = Hoverboard(color: "silver")
print("Hoverboard: \(hoverboard.description)")
// Hoverboard: 0 wheel(s) in a beautiful silver
위의 예시는 super.init()를 호출하지 않는다. 그러나 암묵적으로 호출하게 된다.
Automatic Initializer Inheritance
위에서 언급했듯이, 자식 클래스는 부모 클래스의 생성자를 기본적으로 상속받지 안흔다.
그러나 특정 조건에 든다면 부모 클래스의 생성자를 자동으로 상속받는다.
전제조건: 클래스의 모든 프로퍼티에 기본 값이 설정되어 있다.
Rule 1
만약 자식 클래스가 Designated initializer를 정의하지 않는다면, 자동으로 부모 클래스의 Designated initializer를 상속받게 된다.
Rule 2
만약 자식 클래스가 부모 클래스의 Designated initializer를 모두 구현하고 있다면 자식 클래스는 부모 클래스의 모든 Convenience initializer를 상속받게 된다.
Designated and Convenience Initializers in Action
이제 예시를 통해서 Designated initializer, Convenience initializer, Automatic initializer inheritance를 보이자
먼저 Base Class인 Food 클래스이다.
class Food {
var name: String
init(name: String) {
self.name = name
}
convenience init() {
self.init(name: "[Unnamed]")
}
}
let namedMeat = Food(name: "Bacon")
// namedMeat's name is "Bacon"
let mysteryMeat = Food()
// mysteryMeat's name is "[Unnamed]"
클래스가 기본 멤버별 생성자가 없기 때문에 name 파라미터를 갖는 Default initializer를 갖고 있다.
또한 init()이라는 convenience initializer를 갖고 있다. 파라미터가 없는 경우이다.
두 번째 클래스는 Food를 상속받는 RecipeIngredient 클래스이다
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)
}
}
let oneMysteryItem = RecipeIngredient()
let oneBacon = RecipeIngredient(name: "Bacon")
let sixEggs = RecipeIngredient(name: "Eggs", quantity: 6)
// 3개 다 아주 정상적으로 할당된다.
이 클래스는 init(name: String, quantity: Int)
Designated initializer를 갖고 있다. 자신이 갖고 있는 quantity 프로퍼티에 값을 할당하고 부모 클래스의 생성자를 호출함으로써 safety check 1을 잘 준수하고 있다.
이 클래스는 또한 init(name: String)
Convenience initializer를 갖고 있다.
이 생성자는 다시금 자기 자신의 Designated initializer를 호출한다.
Convnience initializer가 슈퍼 클래스의 Designated initializer와 같은 모습을 띄고 있기 때문에 override
표기를 해주어야 한다.
RecipeIngredient가 init(name: String)
생성자를 convenience initializer로 만들었더라도, RecipeIngredient 클래스가 부모 클래스의 모든 Designated initializer를 구현하였기 때문에, 부모 클래스가 가진 모든 convenience initializer를 상속받을 수 있다.
class ShoppingListItem: RecipeIngredient {
var purchased = false
var description: String {
var output = "\(quantity) x \(name)"
output += purchased ? " ✔" : " ✘"
return output
}
}
ShoppingListItem은 프로퍼티에 이미 기본 값들이 할당되어 있고 생성자를 정의하지 않았기 때문에, 부모 클래스가 가진 모든 Designated initialier와 Convenience initializer를 상속받는다.
var breakfastList = [
ShoppingListItem(),
ShoppingListItem(name: "Bacon"),
ShoppingListItem(name: "Eggs", quantity: 6),
]
breakfastList[0].name = "Orange juice"
// 이 코드를 하지 않으면 [Unnamed] 로 있을 것이다.
breakfastList[0].purchased = true
for item in breakfastList {
print(item.description)
}
// 1 x Orange juice ✔
// 1 x Bacon ✘
// 6 x Eggs ✘
다음편 보기
2022.02.02 - [Swift] - [Swift] Initialization - 3편 (Failable Initializers)
'Swift' 카테고리의 다른 글
[Swift] Attribute - @discardableResult (0) | 2022.02.09 |
---|---|
[Swift] Initialization - 3편 (Failable Initializers) (0) | 2022.02.02 |
[Swift] Initialization - 1편 (0) | 2022.01.24 |
[Swift] Structure vs Class 무엇을 골라야 할까? (0) | 2022.01.14 |
[Swift] Queues & Threads - Concurrency by Tutorials 2편 (0) | 2022.01.13 |