[iOS] Initializer - 03

Sangwon Shin·2021년 12월 15일
0

iOS

목록 보기
6/9

👨‍👧‍👧 Initializer Inheritance and Overriding

지난시간에 이어서 이니셜라이저를 포함한 클래스를 상속할 때, 이니셜라이저 처리에 대해서 알아보겠습니다.

지난글1, 지난글2에서 클래스타입의 이니셜라이저 위임 규칙이 있고, 클래스를 상속받아도 부모 클래스의 이니셜라이저를 자동으로 상속받지 않는다고 공부했습니다.

오늘은 조금 더 깊게 들어가보겠습니다.

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

let vehicle = Vehicle()
print(vehicle.description) //0

class Bicycle: Vehicle {
    var speed: Double
    init(speed: Double) {
        self.speed = speed
        super.init()
    }
    override init() {
        self.speed = 0
        super.init()
        self.numberOfWheels = 3
    }
}
let bicycle = Bicycle(speed: 2.0)
let bicycle2 = Bicycle()

Vehicle 클래스를 상속받는 Bicycle 의 경우, 자식 클래스의 고유한 저장 프로퍼티가 존재하기 때문에 이니셜라이저에서 부모 클래스의 지정 이니셜라이저를 호출하기 전에 해당 저장 프로퍼티 값을 초기화 해야합니다. (오버로딩)

부모 클래스의 이니셜라이저를 상속 받는 경우는 어떨까요?
앞선 경우와 마찬가지로, 클래스 타입 이니셜라이저 위임의 안전 규칙을 준수해야 합니다. (오버라이딩이므로 이니셜라이저의 매개변수까지 동일해야 합니다.)


🖥 Automatic Initializer Inheritance

금방 클래스 타입의 이니셜라이저는 자동으로 상속되지 않는다고 했습니다. 하지만 특정상황에서는 자동으로 상속 받습니다. 🤚멈춰..

자식 클래스에서 프로퍼티 기본값을 모두 제공한다고 가정할때 두 가지 규칙에 따라 이니셜라이저가 자동으로 상속됩니다.

  • 자식 클래스에서 별도의 이니셜러라이저를 구현하지 않는다면, 부모 클래스의 지정 이니셜라이저가 자동으로 상속됩니다.
  • 자식 클래스가 부모 클래스의 지정이니셜라이저를 모두 구현한 경우 (+자동으로 상속된 경우포함) 자동으로 부모 클래스의 편의 이니셜라이저가 자동으로 상속됩니다.

무슨말일까요.. 글로만 보니까 난해합니다. 코드와 함께 확인해보겠습니다.

class Person {
    var name: String
    
    init(name: String) {
        self.name = name
    }
    
    convenience init() {
        self.init(name: "Unknown")
    }
}

class Student: Person {
    var major: String = "Swift"
}

let sangwon: Person = Person(name: "sangwon")
print(sangwon.name) //sangwon

let hoonie: Student = Student(name: "Lee")
print(hoonie.name) //Lee

let sanghuyn: Student = Student()
print(sanghuyn.name) //Unknown

부모 클래스 Person 은 지정 이니셜라이저와 편의 이니셜라이저가 존재합니다. Person을 상속받는 Student 자식 클래스는 major 라는 저장 프로퍼티가 존재하지만 기본값을 제공합니다.

앞선 조건을 만족하기 때문에 자동으로 지정 이니셜이저를 상속받습니다.(조건1)
그렇기 때문에 let hoonie: Student = Student(name: "Lee") 를 사용할 수 있습니다.

조금만 더 생각해보면 자식 클래스에 존재하는 저장 프로퍼티는 이미 기본값을 가지기 때문에 인스턴스 생성 시 1 단계를 진행하는데 아무런 문제가 없습니다.

조건2에 따라서 let sanghuyn: Student = Student() 는 부모 클래스의 편의 이니셜라이저를 상속받아 사용할 수 있습니다.

공식문서 예제를 하나만 더 보겠습니다.

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


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

class ShoppingListItem: RecipeIngredient {
    var purchased = false
    var description: String {
        var output = "\(quantity) x \(name)"
        output += purchased ? " ✔" : " ✘"
        return output
    }
}
let test1 = ShoppingListItem()
let test2 = ShoppingListItem(name: "zzamgbbong")
let test3 = ShoppingListItem(name: "zzazangmyun", quantity: 2, qu: 1)

print(test1.name)
print(test2.name)
print(test3.name)

앞선 예제보다 쪼오금 복잡합니다.

RecipeIngredient 클래스를 먼저 확인해보겠습니다.

RecipeIngredient 는 quantity 라는 기본값이 설정되지 않은 저장 프로퍼티를 가지고 있지만 이니셜라이저를 통해서 초기화 했고, 부모 클래스의 지정 이니셜라이저를 오버라이딩해서 부모 클래스의 지정 이니셜라이저를 사용할 수 있기 때문에 부모 클래스의 편의 이니셜라이저를 상속 받습니다.🥲

ShoppingListItem 클래스는 앞선 예제와 동일한 상황입니다.
purchased, description 프로퍼티는 기본값을 가지고 있기 때문에 부모 클래스의 지정 이니셜라이저와 편의 이니셜라이저를 자동으로 상속받습니다.

그래서 위와 같이 ShoppingListItem 클래스를 통해서 인스턴스를 3가지 방법을 통해서 생성할 수 있습니다.


✂️ Failable Initializers

이니셜라이저를 통해서 인스턴스를 생성할 때 실패할 가능성이 있는 이니셜라이저를 init? 을 통해서 표현 할 수 있습니다.

class Product {
    let name: String
    init?(name: String) {
        if name.isEmpty { return nil }
        self.name = name
    }
}

class CartItem: Product {
    let quantity: Int
    init?(name: String, quantity: Int) {
        if quantity < 1 { return nil }
        self.quantity = quantity
        super.init(name: name)
    }
}

if let oneUnnamed = CartItem(name: "", quantity: 1) {
    print("Item: \(oneUnnamed.name), quantity: \(oneUnnamed.quantity)")
} else {
    print("Unable to initialize one unnamed product")
}
// Prints "Unable to initialize one unnamed product"

위와 같은 경우 빈 문자열을 통해 인스턴스를 생성하게 되면 nil 값을 반환하고 초기화를 실패하게 됩니다.

class Document {
    var name: String?
    init() {}
    init?(name: String) {
        if name.isEmpty { return nil }
        self.name = name
    }
}

class AutomaticallyNamedDocument: Document {
    override init() {
        super.init()
        self.name = "[Untitled]"
    }
    override init(name: String) {
        super.init()
        if name.isEmpty {
            self.name = "[Untitled]"
        } else {
            self.name = name
        }
    }
}

class UntitledDocument: Document {
    override init() {
        super.init(name: "[Untitled]")!
    }
}

실패가능한 이니셜라이저를 상속하는 경우도 살펴보겠습니다.

Document 클래스를 보면 name 이라는 프로퍼티는 옵셔널값으로
실패불가능한 init()을 통해서는 name에 nil 값이 저장되고
실패가능한 이니셜라이저 init?(name: String) 을 통해서는 빈문자열일 경우 nil 값을 반환하는 형태입니다.

AutomaticallyNamedDocument 에서 실패 가능한 init(name: String) 을 상속받아 실패 불가능한 형태로 변경하고 있습니다.
❗️역관계는 불가능합니다.

UntitledDocument 에서는 실패 불가능한 init() 을 상속받기 때문에 실패가능한 이니셜라이저 init(name: String) 을 사용해 초기화를 진행하되 무조건 실패 불가능하도록 옵셔널 언래핑을 해주는 것을 확인 할 수 있습니다.


🏷 P.S.

길고 길었던 이니셜라이저 정리가 끝났습니다.
아직 정확하게 모든 개념들을 이해한건 아니지만 이제 어느정도 갈피를 잡았습니다😺

내일부터 얼른 스토리보드 사용없이 코드로 오토레이아웃 및 UI 를 잡는법을 정리하도록 하겠습니다. 🔥🔥🔥🔥🔥🔥

profile
개발자가 되고싶어요

0개의 댓글