이전 기사 다음에 Initializers를 살펴 보겠습니다.
클래스 상속 및 초기화 (Class Inheritance and Initialization)
이전 기사에서는 클래스가 값 유형의 초기화보다 복잡하다고 말했습니다.
Why?
즉시 상속 가능한 클래스는 자체 저장 속성뿐만 아니라 슈퍼 클래스로부터 상속한 모든 프로퍼티의 초기치도 반드시 할당할 필요가 있기 때문입니다.
.
따라서 클래스의 초기화 프로세스는 비교적 복잡합니다.
클래스 유형을 초기화하는 두 가지 방법을 지원합니다.
– 지정 이니셜라이저(Desingate Initializers)와 편리한 이니셜라이저(Convenience Initializers)
- 지정 이니셜라이저는 보통 우리가 알고 있는 이니셜라이저라고 생각하면 좋다.
구조체와 마찬가지로 init 메소드로 정의합니다. - 편리한 이니시에이터의 정의는 기본 이니셜라이저와 같지만 init 앞에 convenience 키워드를 붙여준다.
// 지정 이니셜라이저
init(parameters) {
statements
}
// 편리한 이니셜라이저
convenience init(parameters) {
statements
}
유용한 이니셜 라이저는 왜 있습니까? 이하의 이니셜라이저 위임을 보면 알 수 있다.
– 클래스 타입의 이니셜 라이저 위임 (Initializer Delegation for Class Types)
값형의 이니셜라이저 위임은, 다른 init 메소드중에서 다른 init 메소드를 호출해 사용했습니다.
수업의 경우 위임하는 방법을 살펴 보겠습니다.
클래스 이니셜라이저는 다음 세 가지 규칙을 따릅니다.
- 규칙 1 지정 이니셜라이저는 반드시 자신의 superclass 지정 이니셜라이저를 호출해야 한다.
- 규칙 2 유용한 이니셜라이저는 같은 클래스의 다른 이니셜라이저를 호출해야 합니다.
- 규칙 3 유용한 이니셜라이저는 궁극적으로 지정된 이니셜라이저를 호출해야 합니다.
Swift Language Guide에는 좋은 그림이 있다.
규칙 1과 같이 지정 이니셜라이저는 superclass의 지정 이니셜라이저를 호출해, 편리한 이니셜라이저는 다른 편리한 이니셜라이저를 호출해, 결국 자신의 지정 이니셜라이저를 호출하고 있다.
이것은, 상기의 클래스의 초기화에서는, 상위 클래스의 모든 프로퍼티의 초기치를 설정할 필요가 있기 때문입니다.
– 2단계 초기화(Two-Phase Initialization)
Swift의 클래스 초기화는 2단계 프로세스를 거칩니다.
1단계 초기화
: 모든 속성이 초기값으로 초기화되는 단계.
이 단계에서 인스턴스화를 위한 메모리가 할당되어 자신의 모든 프로퍼티를 우선 초기화한 후, 부모 뷰의 이니셜라이저를 호출해, 이 과정은 최상단 클래스의 프로퍼티까지 모두 초기치가 할당될 때까지 진행됩니다 한다.
2단계 초기화
: 1단계 초기화가 완료된 후 속성을 커스터마이즈하는 단계이다.
1단계에서는 자신을 포함한 모든 상위 뷰의 속성에 대한 초기 값이 할당되어 있으므로 이를 설정할 수 있습니다.
Swift는 이 2단계의 안전을 확보하기 위해 4단계의 safety-check를 실시한다.
- 지정 이니셜라이저는, 클래스내에서 슈퍼 클래스의 이니셜라이저에 위임하기 전에 모든 프로퍼티을 초기화할 필요가 있습니다.
- 지정 이니셜라이저는 상속된 값을 할당하기 전에 수퍼클래스 이니시에이터에 위임을 전달해야 합니다.
- 유용한 이니셜라이저는 다른 속성을 할당하기 전에 다른 이니셜라이저에 위임해야 합니다.
- 이니셜라이저는 초기화의 1단계가 끝나기 전에 self 값을 참조하거나 다른 인스턴스 속성 메소드를 호출하거나 읽을 수 없습니다.
조금 복잡하지만 (많이 복잡했습니다 …)
최종적으로 초기화를 진행하고, 모든 프로퍼티에 초기값을 할당하고, 그 값을 가공하는 과정을 1단계와 2단계로 나누어 safety-check를 통해 안전하게 진행하는 것이다.
(초기값의 할당전에 액세스하거나, 다른 상위 클래스에 의해 값이 변질하는 상황 등을 방지한다.
)
– 이니셜 라이저 상속 및 오버라이딩(Initializer Inheritance and Overriding)
Swfit의 서브 클래스는 기본적으로 슈퍼 클래스 이니셜 라이저를 상속하지 않음라고 한다.
슈퍼 클래스의 이니셜라이저가 무분별하게 계승되어 복잡해지는 것을 막기 위해서다.
서브클래스에서 별도의 이니셜라이저를 정의하지 않아도 슈퍼클래스 이니셜라이저를 사용하여 인스턴스를 초기화할 수 있습니다.
슈퍼 클래스 이니셜 라이저를 재정의하려면 서브클래스에서 override 키워드로 재정의.
– 자동 이니시에이터 인스턴스(Automatic Initializer Inheritance)
위의 설명대로 기본적으로 슈퍼 클래스의 이니셜 라이저를 상속하지 않지만 특정 조건의 경우 자동으로 상속됩니다.
해당 클래스의 모든 속성에 기본값을 할당한 경우 두 가지 규칙을 따라야 합니다.
- 서브 클래스가 지정 이니셜라이저를 정의하지 않는 경우, 자동적으로 슈퍼 클래스의 지정 이니셜라이저를 상속합니다.
- 서브 클래스가 슈퍼 클래스의 지정 이니셜라이저를 모두 구현했을 경우, 자동적으로 슈퍼 클래스의 편의 이니셜라이저를 상속한다.
결국, 상위 클래스의 초기화를 위해서 불필요한 이니셜라이저를 오버라이드(override) 하거나 구현할 필요는 없습니다.
예를 통해 확인해 봅시다.
그건 그렇고,이 부분을 이해하는 것은 정말 어려웠습니다 …
예 1
class Person {
var name: String
init(name: String) {
self.name = name
}
convenience init() {
self.init(name: "Unknwon")
}
}
class Student: Person {
var major = "Swift"
}
let jounrney: Student = Student()
let go: Student = Student(name: "go")
코드를 보면 Person을 상속한 Student가 major 변수에 기본값을 할당했기 때문에 지정된 초기화기를 정의하지 않았습니다.
그 때문에, 룰 1번에 해당해 슈퍼 클래스의 이니셜라이저를 계승했다.
혼란 포인트!
: 규칙 1의 설명을 보면, 지정 이니셜라이저를 계승한다고 했지만, 편의 이니셜라이저도 계승하고 있다.
예 2
class Person {
var name: String
init(name: String) {
self.name = name
}
convenience init() {
self.init(name: "Unknwon")
}
}
class Student: Person {
var major: String
override init(name: String) {
self.major = "Unknwon"
super.init(name: name)
}
init(name: String, major: String) {
self.major = major
super.init(name: name)
}
}
let jounrney1: Student = Student()
let jounrney2: Student = Student(name: "저니")
let jounrney3: Student = Student(name: "저니", major: "Swfit")
Student는 Person을 상속하고 Person의 지정 이니셜 라이저를 재정의하고 정의했습니다.
따라서 자동으로 편의 이니셜 라이저가 상속됩니다.
예 3
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)
}
}
let oneMysteryItem = RecipeIngredient()
let oneBacon = RecipeIngredient(name: "Bacon")
let sixEggs = RecipeIngredient(name: "Eggs", quantity: 6)
Food를 계승한 RecipeIngredient는, Food의 지정 이니셜라이저인 init(name: String)를 convenience로 오버라이드(override) 해 사용했다.
따라서 Food의 편리 이니셜 라이저가 자동으로 상속됩니다.
그림을 보면 초기화 과정의 흐름을 알 수 있다.
이와 같이 클래스의 초기화 과정에 대해 배웠다.
확실히, 값 타입의 초기화 프로세스보다 훨씬 복잡한 것을 알 수 있습니다.
그래서 조금 혼란스러운 부분이 있어 반복 학습과 실제 응용을 많이 해봐야 한다.
실제로 이니셜라이저의 정리를 1과 2로 나누어 정리하려고 했습니다.
생각보다 내용 길고 3까지 나누어 정리하도록 하자~