클래스 상속 및 초기화
클래스의 모든 저장속성에는 초기화 중에 초기값이 할당 되어야 한다. 이 때 초기화 대상에는 클래스가 상속받는 상위클래스의 속성 또한 포함된다.
Swift는 속성 초기화를 위해 두 가지 종류의 이니셜라이저를 정의하는데,
- 지정 이니셜라이저(designated initializer)
- 편의 이니셜라이저(convenience initializer)
로 알려져 있다.
지정 이니셜라이저는 클래스의 기본 이니셜라이저이다. 모든 속성을 초기화하고 적정한 수퍼클래스 이니셜라이저를 호출하여 초기화 프로세스를 진행한다.
초기화가 발생하고 수퍼클래스 초기화 체인까지 계속되는 “funnel”지점이다.
모든 클래스에는 지정이니셜라이저가 하나 이상 있어야 하며, 경우에 따라 수퍼클래스에서 이니셜라이저를 상속함으로서 조건충족을 하기도 한다.
편의 이니셜라이저는 클래스에서 사용되는 보조 이니셜라이저다.
클래스에서 필요로하지 않는 경우 이니셜라이저를 제공할 필요가 없다.
지정이니셜라이저 syntax
init(parameters) {
}
편의 이니셜라이저 syntax
init앞으로 convenience키워드를 둔다.
convenience init(parameters) {
//self.init(...
}
클래스 유형에 대한 초기화 위임
지정 이니셜라이저와 편의 이니셜라이저 간의 관계를 단순화 하기 위해 Swift는 이니셜라이저 간 위임 호출에 대해 세 가지 규칙을 적용한다.
중요!
- 지정된 이니셜라이저는 직계 슈퍼 클래스에서 지정된 이니셜라이저를 호출해야만 한다.
- 편의 이니셜라이저는 동일한 클래스의 다른 이니셜라이저를 호출해야 한다.
- 편의 이니셜라이저는 궁극적으로 지정 이니셜라이저를 호출해야한다.
위 규칙은 아래 그림으로 표현할 수 있다.
코드로 규칙을 적용해본다면 다음과 같다.
struct Person {
var name: Name
}
struct Name {
var first: String
var last: String
}
class SomeA {
var firstName: String
init(firstName: String) {
self.firstName = firstName
}
}
class SomeB: SomeA {
var lastName: String
init(name: Name) {
self.lastName = name.last
super.init(firstName: name.firstName)
}
convenience init(person: Person) {
self.init(name: person.name)
}
}
let b = SomeB(person: Person(name: Name(first: "Kim", last: "Seongjun")))
print(b.firstName)
print(b.lastName)
2단계 초기화
Swift의 초기화는 2단계 프로세스다.
- 각 저장속성에 대한 초기값이 할당되고
- 새 인스턴스가 준비된 것으로 간주되기 전 각 클래스에 저장된 속성을 추가로 사용자 지정할 수 있는 기회가 주어진다.
Swift의 컴파일러는 2단계 초기화가 오류없이 완료되는지 확인하기 위해 4가지 검사를 수행한다.
안전점검 1
지정이니셜라이저는 상위 클래스 이니셜라이저를 호출하기 전 해당 클래스(서브클래스)에서 모든 속성이 초기화 되었는지 확인한다.
❌ - 상위 클래스의 이니셜라이저 호출 전 last가 초기화 되지 않았으므로 컴파일 오류
var last: String
init(name: Name) {
super.init(first: name.first) //!! ❌Property 'self.last' not initialized at super.init call
self.last = name.last
}
⭕️ - last를 초기화하고 편안허게 상위 클래스 이니셜라이저 호출!
var last: String
init(name: Name) {
self.last = name.last
super.init(first: name.first)
}
안전점검 2
지정 이니셜라이저는 상속된 속성에 값을 할당하기 전 상위 클래스 이니셜라이저를 초기화 해야한다.
그렇지 않은 경우 할당한 새 값은 자체 초기화로 인해 슈퍼클래스가 덮어 쓰게 된다.
init(name: Name) {
self.last = name.last
super.first = "Kay" //!! ❌'self' used in property access 'first' before 'super.init' call
super.init(first: name.first)
}
안전점검 3
편의 이니셜라이저는 속성에 값을 할당 하기 전에 다른 이니셜라이저를 호출해야한다.
convenience init(person: Person) {
self.last = "Jun" // !! ❌'self' used before 'self.init' call or assignment to 'self'
self.init(name: person.name)
}
안전점검 4
self초기화는 초기화의 첫 번째 단계가 완료될 때까지 인스턴스 메서드를 호출하거나 인스턴스 속성의 값을 읽거나 값을 참조할 수 없다.
위 네가지 안전검사를 기반으로 2단계 초기화가 실행되는 방식은 다음과 같다.
1단계
클래스에서 지정 또는 편의 이니셜라이저가 호출됩니다.
해당 클래스의 새 인스턴스에 대한 메모리가 할당됩니다. 메모리가 아직 초기화되지 않았습니다.
해당 클래스의 지정된 이니셜라이저는 해당 클래스에 의해 도입된 모든 저장된 속성에 값이 있음을 확인합니다. 이제 이러한 저장된 속성에 대한 메모리가 초기화됩니다.
지정된 이니셜라이저는 자신의 저장된 속성에 대해 동일한 작업을 수행하기 위해 수퍼클래스 이니셜라이저에 전달합니다.
이것은 체인의 맨 위에 도달할 때까지 클래스 상속 체인을 계속합니다.
체인의 맨 위에 도달하고 체인의 마지막 클래스가 저장된 모든 속성에 값이 있음을 확인하면 인스턴스의 메모리가 완전히 초기화된 것으로 간주되고 1단계가 완료됩니다.
2단계
체인의 맨 위에서 다시 아래로 작업하면 체인의 각 지정된 이니셜라이저에는 인스턴스를 추가로 사용자 정의할 수 있는 옵션이 있습니다.
이니셜라이저는 이제 해당 속성에 액세스하고 속성을 수정하고 인스턴스 메서드를 호출하는 등의 작업을 수행할 수 있습니다.
마지막으로, 체인의 모든 편의 이니셜라이저는 인스턴스를 사용자 정의하고 로 작업할 수 있는 옵션이 있습니다.
다음은 1단계에서 가상의 하위 클래스와 상위 클래스에 대한 초기화 호출을 찾는 방법입니다.
지정된 이니셜라이저는 안전 검사 1에 따라 모든 하위 클래스 속성에 값이 있는지 확인합니다. 그런 다음 상위 클래스에서 지정된 이니셜라이저를 호출하여 체인 위로 초기화를 계속합니다.
슈퍼클래스의 지정된 이니셜라이저는 모든 슈퍼클래스 속성에 값이 있는지 확인합니다.
슈퍼클래스의 모든 속성이 초기 값을 가지면 해당 메모리는 완전히 초기화된 것으로 간주되고 1단계가 완료됩니다.
2단계에서 동일한 초기화 호출을 찾는 방법은 다음과 같습니다.
슈퍼클래스의 지정된 이니셜라이저는 이제 인스턴스를 추가로 커스터마이즈할 수 있는 기회가 있습니다(꼭 그럴 필요는 없습니다).
슈퍼클래스의 지정된 이니셜라이저가 완료되면 서브클래스의 지정된 이니셜라이저가 추가 사용자 정의를 수행할 수 있습니다(다시 말하지만 꼭 그래야 하는 것은 아닙니다).
마지막으로, 서브클래스의 지정된 이니셜라이저가 완료되면 원래 호출된 편의 이니셜라이저가 추가 사용자 정의를 수행할 수 있습니다.
이니셜라이저 상속 및 재정의
ObjC와 달리 Swift에서 하위 클래스는 상위 클래스 이니셜라이저를 상속하지 않는다.
상위클래스의 이니셜라이저와 똑같은 이니셜라이저를 자식클래스에서 사용하고 싶다면 하위클래스에서 부모의 이니셜라이저와 똑같은 이니셜라이저를 구현하면 된다.
상위클래스와 동일한 지정이니셜라이저를 하위클래스에 구현할 때 재정의(override)하면 된다.
override키워드를 이니셜라이저 앞에 붙인다.
하위 클래스의 편의 이니셜라이저가 상위 클래스의 지정 이니셜라이저를 재정의 하는 경우에도 override키워드를 사용한다.
상위 클래스의 편의 이니셜라이저와 동일한 이니셜라이저를 하위 클래스에서 구현할 때 override 를 붙이지 않는다.
하위 클래스에서 상위 클래스의 편의 이니셜라이저는 호출할 수 없기 때문이다.
class Person {
var name: String = ""
var age: Int = 0
init(name: String, age: Int) {
self.name = name
self.age = age
}
convenience init(name: String) {
self.init(name: name, age: 0)
}
}
class Student: Person {
var major: String = "F"
override init(name: String, age: Int) {
self.major = "Swift"
super.init(name: name, age: age)
}
convenience init(name: String) {
self.init(name: name, age: 4)
}
}
상위 클래스가 지정이니셜라이저를 제공하지 않을때 하위 클래스에서 지정이니셜라이저를 작성해보자.
class Person {
}
class Student: Person {
var className: String
init(className: String) {
self.className = className
//암시적으로 상위 클래스 기본 이니셜라이저 init() 호출
}
}
상속하는 상위 클래스의 이니셜라이저를 암시적으로 호출한다.
상위 클래스의 실패가능한 이니셜라이저를 재정의할 경우 실패 가능한 이니셜라이저로 재정의해도 되고 실패하지 않는 이니셜라이저로 재정의 해 줄수도 있다.
class A {
init?() {
}
}
class B: A {
override init?() {
}
}
자동 이니셜라이저 상속
자식클래스에서 속성 기본값을 모두 제공한다는 가정하에 , 다음 두가지 규칙에 따라 이니셜라이저가 자동 상속 된다.
규칙1
하위 클래스에서 별도의 지정 이니셜라이저를 구현하지 않는 다면 상위 클래스의 지정 이니셜라이저가 자동 상속 된다.
class Person {
var name?
init(name: String) {
}
}
class Student: Person {
}
let student = Student(name: "") //상위 클래스의 지정이니셜라이저
하위 클래스에서 지정이니셜라이저를 정의했다면 자동상속이 되지 않는다.
class Person {
var name: String?
init(name: String) {
}
}
class Student: Person {
init(some: Some) {
super.init(name: some.name)
}
}
let student = Student(some: Some()) //❌Student(name: ... 불가
규칙2
* 규칙 1에 따라 하위 클래스에서 상위 클래스 지정이니셜라이저를 자동으로 상속 받은 경우
class Person {
var name: String = ""
var age: Int = 0
init(age: Int) {
}
init(name: String, age: Int) {
}
convenience init(name: String) {
self.init(name: name, age:0)
}
}
class Student: Person {
}
let student = Student(name: "") // 규칙2 조건 충적이 되어 편의 이니셜라이저 사용 가능
* 상위 클래스의 지정이니셜라이저를 모두 재정의 하여 상위 클래스와 동일한 지정 이니셜라이저를 모두 사용할 수 있는 상황일 때 부모 클래스의 편의 이니셜라이저가 모두 자동상속 된다.
class Person {
var name: String = ""
var age: Int = 0
init(age: Int ) {
self.age = age
}
init(name: String) {
self.name = name
}
convenience init(age: Int, name: String) {
}
}
class Student: Person {
override init(name: String) {
//...
}
override init(hei: Int) {
}
}
let student = Student(age: 1, name: "Kim) //지정이니셜라이저를 모두 재정의함. 편의 이니셜라이저 사용 가능.(자동상속됨)
이니셜라이저 체인
class Food {
var name: String
init(name: String) {
self.name = name
}
convenience init() {
self.init(name: "Unnamed")
}
}
지정이니셜라이저에서 name을 초기화 해준 상황
편의 이니셜라이저는 지정이니셜라이저로 “Unnamed” 문자열을 보내 name을 초기화 하도록 하고 있다.
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)
}
}
요리재료 클래스는 Food클래스의 지정이니셜라이저를 재정의 함으로서 Food의 편의이니셜라이저를 자동 상속 받게 되었다.
let recipeIngredient = RecipeIngredient()
print(recipeIngredient.name) // "Unnamed"
RecipeIngredient()는 상위클래스(Food)의 편의이니셜라이저를 호출한다.
RecipeIngredient를 인스턴스화 하는 방법 세 가지를 보자
let oneMysteryItem = RecipeIngredient()
let oneBacon = RecipeIngredient(name: "Bacon")
let sixEggs = RecipeIngredient(name: "Eggs", quantity: 6)
또 다른 클래스를 예시로 보자.
class ShoppingListItem: RecipeIngredient {
var purchased = false
var description: String {
var output = "\(quantity) x \(name)"
output += purchased ? "O" : "X"
return output
}
}
ShoppingListItem은 상위 클래스의 지정이니셜라이저를 따로 정의하지 않았기 때문에 상위 클래스의 지정이니셜라이저를 모두 자동상속한다.
var breakfastList = [
ShoppingListItem(), //Unnamed
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 ✘
'난 iOS개발자 > iOS' 카테고리의 다른 글
Class의 성능을 향상 시키는 방법 (0) | 2022.05.03 |
---|---|
initialization -3 (0) | 2022.04.29 |
initialization-1 (0) | 2022.04.25 |
Method( Instance Method, Type Method) (0) | 2022.03.16 |
백그라운드모드에서 위치정보 가져오기 (BackgroundModes + LocationUpdates) (0) | 2022.03.04 |