[Swift5] Initialization 3

Junyoung Park·2022년 3월 23일
0

Swift5 Docs

목록 보기
30/37
post-thumbnail
  • 다음은 Swift 5.6 Doc의 Initialization 공부 내용을 정리했음을 밝힙니다.

Initialization

클래스 상속 및 초기화

자동 이니셜라이저 상속

자식 클래스가 부모 클래스를 상속했다 할지라도 이니셜라이저 역시 자동으로 상속받지 않는다. 자식 클래스에만 존재하는 프로퍼티 디폴트 값을 적용할 때 두 가지 규칙이 있다.

  1. 자식 클래스가 지정된 이니셜라이저를 사용하지 않으면 부모 클래스의 지정된 이니셜라이저를 자동으로 모두 상속한다.
  2. 자식 클래스는 상속을 통해 부모 클래스의 모든 지정된 이니셜라이저를 사용할 수 있다. 또는 커스텀 이니셜라이저를 사용해 자동으로 부모 클래스의 편리한 이니셜라이저를 모두 상속할 수 있다.

자식 클래스는 부모 클래스의 지정된 이니셜라이저를 자신의 편리한 이니셜라이저로 사용할 수 있다.

지정된 또는 편리한 이니셜라이저 사용 중

부모 및 자식 클래스 간의 상속 관계를 통해 이니셜라이저 사용 방법을 파악해보자.

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]"

Food 클래스는 이름을 입력받아 인스턴스화하는 지정된 이니셜라이저를 제공하고 있다. 지정된 이니셜라이저는 인스턴스의 모든 저장 프로퍼티가 빈 칸 없이 초기화되도록 보장한다. Food 클래스는 부모 클래스가 없기 때문에 초기화를 완료하기 위해 super.init()을 호출할 필요가 없다. 또한 편리한 이니셜라이저 init()을 아규먼트 없이 제공하고 있다.

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)

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 ✘

Food를 상속하는 RecipeIngredient 클래스는 이름과 양을 파라미터로 받아 지정된 이니셜라이저 init()을 사용하고 있다. 이후 init을 딜리게이트해서 중복 코드 사용을 방지하고 있다.

편리한 이니셜라이저는 이름만 파라미터로 넘겼을 때 호출되는데, 이때 자동으로 양은 1로 설정된다. 이때도 중복 코드를 방지하려고 편리한 이니셜라이저가 지정된 이니셜라이저를 딜리게이트하고 있다. 부모 클래스 Food의 편리한 이니셜라이저와 다른 이니셜라이저이기 때문에 오버라이딩해서 사용해야 한다.

결과적으로 RecipeIngredient 클래스는 상위 클래스의 모든 지정된 이니셜라이저 및 편리한 이니셜라이저를 상속 등을 통해 제공받는다.

ShoppingListItem 클래스는 RecipeIngredient 클래스를 상속하는 자식 클래스로 이니셜라이저가 없다. 따라서 부모 클래스의 모든 이니셜라이저를 자동으로 상속한다.

breakfastListShoppingListItem 인스턴스 리터럴 세 개로 구성된 배열이다. 이름 없이 초기화된 인덱스 0 인스턴스는 배열에 들어간 이후 Orange juice로 입력받는다.

이니셜라이저 실패

초기화가 실패할 수 있는 이니셜라이저를 정의하기도 한다. 유효하지 않은 초기화 파라미터 값이 들어오거나 필수적인 외부 리소스가 없을 때와 같이 초기화를 방지해야 할 때가 있다.

초기화 실패 상황을 다루기 위해 코드 상으로 ? 키워드를 붙여야 한다. 그런데 같은 종류의 파라미터 타입과 이름을 사용해서 failablenonfailable 이니셜라이저를 동시에 정의할 수는 없다. 즉 따로 선언해야 한다. 초기화가 실패할 때 실패했음을 알려주기 위해 널 값을 리턴하자(indicator 역할).

let wholeNumber: Double = 12345.0
let pi = 3.14159

if let valueMaintained = Int(exactly: wholeNumber) {
    print("\(wholeNumber) conversion to Int maintains value of \(valueMaintained)")
}
// Prints "12345.0 conversion to Int maintains value of 12345"

let valueChanged = Int(exactly: pi)
// valueChanged is of type Int?, not Int

if valueChanged == nil {
    print("\(pi) conversion to Int doesn't maintain value")
}
// Prints "3.14159 conversion to Int doesn't maintain value"

init(exactly:) 이니셜라이저를 통해 값 변환이 안 될 경우 초기화가 실패한다.

struct Animal {
    let species: String
    init?(species: String) {
        if species.isEmpty { return nil }
        self.species = species
    }
}

let someCreature = Animal(species: "Giraffe")
// someCreature is of type Animal?, not Animal

if let giraffe = someCreature {
    print("An animal was initialized with a species of \(giraffe.species)")
}
// Prints "An animal was initialized with a species of Giraffe"

let anonymousCreature = Animal(species: "")
// anonymousCreature is of type Animal?, not Animal

if anonymousCreature == nil {
    print("The anonymous creature couldn't be initialized")
}
// Prints "The anonymous creature couldn't be initialized"

인스턴스 생성 시 초기화 파라미터로 넘겨진 species가 비었다면 초기화가 실패한다. 이때 널 값과 빈 문자열은 서로 다르다. 빈 문자열은 옵셔널 문자열이 아니기 때문이다.

열거형에서의 초기화 실패

파라미터가 열거 케이스와 맞지 않으면 초기화가 실패한다.

enum TemperatureUnit {
    case kelvin, celsius, fahrenheit
    init?(symbol: Character) {
        switch symbol {
        case "K":
            self = .kelvin
        case "C":
            self = .celsius
        case "F":
            self = .fahrenheit
        default:
            return nil
        }
    }
}

let fahrenheitUnit = TemperatureUnit(symbol: "F")
if fahrenheitUnit != nil {
    print("This is a defined temperature unit, so initialization succeeded.")
}
// Prints "This is a defined temperature unit, so initialization succeeded."

let unknownUnit = TemperatureUnit(symbol: "X")
if unknownUnit == nil {
    print("This isn't a defined temperature unit, so initialization failed.")
}
// Prints "This isn't a defined temperature unit, so initialization failed."

init?, 즉 실패 경우를 정하는 이니셜라이에서 파라미터로 입력받은 symbol이 열거 케이스와 맞지 않으면 널을 리턴, 즉 실패하고 있다.

원시값을 통해 열거형에서의 초기화 실패

init?(rawValue:)로 바로 열거 케이스를 찾을 수도 있다.

enum TemperatureUnit: Character {
    case kelvin = "K", celsius = "C", fahrenheit = "F"
}

let fahrenheitUnit = TemperatureUnit(rawValue: "F")
if fahrenheitUnit != nil {
    print("This is a defined temperature unit, so initialization succeeded.")
}
// Prints "This is a defined temperature unit, so initialization succeeded."

let unknownUnit = TemperatureUnit(rawValue: "X")
if unknownUnit == nil {
    print("This isn't a defined temperature unit, so initialization failed.")
}
// Prints "This isn't a defined temperature unit, so initialization failed."

초기화 실패 전파 과정

초기화 실패 시 같은 타입(클래스, 구조체, 열거형)의 다른 실패 가능한 이니셜라이저로 딜리게이트할 수 있다. 자식 클래스에서 부모 클래스로도 딜리게이트되기 때문에 중복 코드를 줄일 수 있다. 즉 초기화가 실패한다면 전 과정이 실패하기 때문에 컴파일 시간도 줄일 수 있다.

실패 가능한 이니셜라이저는 실패하지 않는 이니셜라이저로 딜리게이트될 수도 있다.

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 twoSocks = CartItem(name: "sock", quantity: 2) {
    print("Item: \(twoSocks.name), quantity: \(twoSocks.quantity)")
}
// Prints "Item: sock, quantity: 2"
If you try to create a CartItem instance with a quantity value of 0, the CartItem initializer causes initialization to fail:

if let zeroShirts = CartItem(name: "shirt", quantity: 0) {
    print("Item: \(zeroShirts.name), quantity: \(zeroShirts.quantity)")
} else {
    print("Unable to initialize zero shirts")
}
// Prints "Unable to initialize zero shirts"

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"

파라미터로 입력받은 quantity가 1 이상이면 유효하지만, 그렇지 않으면 초기화 과정에서 실패하도록 작성되어 있다. name도 빈 문자열이면 실패한다.

실패 가능한 이니셜라이저 오버라이딩

실패 가능한 이니셜라이저도 오버라이딩할 수 있다. 부모 클래스의 실패 가능한 이니셜라이저를 자식 클래스가 nonfailable 이니셜라이저로 오버라이딩할 수도 있다. 이때 부모 클래스 이니셜라이저를 딜리게이트하려면 부모 클래스의 실패 가능한 이니셜라이저 결괏값을 강제로 언래핑해야 한다. 이때 nonfailablefailable로 오버라이딩할 수는 없다.

class Document {
    var name: String?
    // this initializer creates a document with a nil name value
    init() {}
    // this initializer creates a document with a nonempty name value
    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 클래스를 상속하는 AutomaticallyNamedDocument 자식 클래스는 init?을 상속하지만 오버라이딩을 통해 널 값 또는 허용하지 않은 케이스의 경우를 조건문을 통해 제어 가능. 이때 강제 언래핑이 사용되기도 한다.

init!, failable 이니셜라이저

옵셔널 인스턴스를 허용 하는 실패 가능한 이니셜라이저를 정의할 수도 있다. 부모/자식 클래스 관계에서 init?에서 init!을 딜리게이트하거나 오버라이딩할 수 있다. init!은 옵셔널을 허용한 이니셜라이저를 암시적으로 언래핑한 이니셜라이저라고 생각하자.

필수 이니셜라이저

이니셜라이저 앞에 required 키워드를 통해 이 클래스를 상속하는 모든 자식 클래스가 이 이니셜라이저를 사용해야 함을 표시할 수 있다.

class SomeClass {
    required init() {
        // initializer implementation goes here
    }
}

class SomeSubclass: SomeClass {
    required init() {
        // subclass implementation of the required initializer goes here
    }
}

필수적인 지정된 이니셜라이저를 오버라이딩할 때에는 오버라이딩 키워드를 쓰지 않는다.

클로저나 함수로 디폴트 프로퍼티 값을 설정하기

저장 프로퍼티 디폴트 값에 커스터마이징이나 설정이 필요하면 클로저나 전역 함수를 사용할 수 있다. 프로퍼티가 속하는 타입의 새로운 인스턴스가 초기화되면 클로저나 함수가 호출되고 리턴 값이 프로퍼티 디폴트 값으로 할당된다. 이때 클로저나 함수가 프로퍼티와 같은 타입의 임시 값을 생성하고 프로퍼티 디폴트 값으로 리턴한다.

class SomeClass {
    let someProperty: SomeType = {
        // create a default value for someProperty inside this closure
        // someValue must be of the same type as SomeType
        return someValue
    }()
}

클로저 문 마지막 ()을 통해 클로저를 즉시 종료할 수 있다. 괄호가 없으면 클로저가 프로퍼티에 할당되버린다.

프로퍼티 초기화로 클로저를 사용할 때 클로저가 실행되는 시점에서는 아직 인스턴스 초기화가 완료되지 않는다는 점도 기억하자. 즉 클로저 내부에서 프로퍼티 값에 접근할 수 없다는 말이다. 셀프 프로퍼티나 인스턴스 메소드도 사용 불가다.

struct Chessboard {
    let boardColors: [Bool] = {
        var temporaryBoard: [Bool] = []
        var isBlack = false
        for i in 1...8 {
            for j in 1...8 {
                temporaryBoard.append(isBlack)
                isBlack = !isBlack
            }
            isBlack = !isBlack
        }
        return temporaryBoard
    }()
    func squareIsBlackAt(row: Int, column: Int) -> Bool {
        return boardColors[(row * 8) + column]
    }
}

let board = Chessboard()
print(board.squareIsBlackAt(row: 0, column: 1))
// Prints "true"
print(board.squareIsBlackAt(row: 7, column: 7))
// Prints "false"

인스턴스를 생성할 때마다 클로저가 실행되고 디폴트 값이 리턴된다. 앞에서 말한 것처럼 클로저는 이때 임시 배열을 사용해 연산한다.

profile
JUST DO IT

0개의 댓글