공식 문서로 공부하는 Swift (13) - 초기화

ci·2020년 5월 30일
1

Initialization

초기화(Initialization)은 클래스, 구조체, 열거형의 인스턴스를 사용하기 위해 준비하는 과정이다. 각각의 저장 프로퍼티에 초기 값을 설정하고, 새로운 인스턴스의 사용이 준비되기 전에 필요한 다른 세팅이나 초기화 작업을 실행한다.

이니셜라이저(initializer)를 정의해서 초기화 과정을 구현한다. 이는 특정 타입의 새로운 인스턴스를 생성하기 위해 호출할 수 있는 특별한 메소드와 같다. Objective-C의 이니셜라이저와 달리 Swift의 이니셜라이저는 값을 반환하지 않는다. 이니셜라이저의 가장 큰 역할은 타입의 새로운 인스턴스가 처음으로 사용되기 전에 정확히 초기화됐는지를 보장하는 것이다.

클래스 타입의 인스턴스는 디이니셜라이저(deinitializer)를 구현할 수도 있다. 이는 클래스의 인스턴스가 할당을 해제하기 전에 청소를 하는 것이다.



저장 프로퍼티에 초기 값 설정하기

클래스와 구조체는 인스턴스가 생성되는 시점에 반드시 모든 저장 프로퍼티에 적당한 초기값을 설정해야 한다. 저장 프로퍼티는 미결정된 상태로 남겨질 수 없다.

이니셜라이저를 통해 저장 프로퍼티에 값을 설정하거나 프로퍼티 정의의 일부분으로써 프로퍼티 기본 값을 할당할 수 있다.

저장 프로퍼티에 기본 값을 할당하거나 이니셜라이저로 초기 값을 설정할 때, 프로퍼티의 값은 프로퍼티 옵저저버를 호출하지 않고 직접적으로 설정된다.


이니셜라이저 (Initializer)

이니셜라이저(initializer)는 특정 타입의 새로운 인스턴스를 생성한다. 가장 간단한 형태의 이니셜라이저는 init 키워드를 매개 변수가 없는 인스턴스 메소드처럼 작성하는 것이다.

init() {
    // perform some initialization here
}

아래 예시는 화씨 온도를 저장하기 위한 Fahrenheit 구조체를 정의한다. Fahrenheit 구조체는 하나의 저장 프로퍼티인 temperature을 갖고 있다.

struct Fahrenheit {
    var temperature: Double
    init() {
        temperature = 32.0
    }
}
var f = Fahrenheit()
print("The default temperature is \(f.temperature)° Fahrenheit")
// Prints "The default temperature is 32.0° Fahrenheit"

구조체는 init 키워드로 매개 변수가 없는 하나의 구조체를 정의한다. 이 이니셜라이저 안에서 temperature를 32.0으로 초기화 한다.


프로퍼티 기본 값

이니셜라이저에서 저장 프로퍼티에 초기 값을 설정하는 대신 프로퍼티 선언에서 기본 값을 할당할 수 있다. 프로퍼티가 정의될 때 초기 값을 기본 프로퍼티 값으로써 할당한다.

만약 프로퍼티가 항상 같은 초기 값을 가진다면, 기본 값을 제공하는 것이 이니셜라이저를 사용하는 것보다 낫다. 결과는 동일하지만, 기본 값은 선언보다 프로퍼티의 초기화에 더 밀접하게 연결되어 있기 때문이다. 이는 더 짧고 명확하며, 기본 값으로부터 프로퍼티의 타입을 추론할 수 있게 해 준다. 기본 값은 또한 기본 이니셜라이저와 이니셜라이저 상속의 이점을 더 쉽게 가져 갈 수 있게 만든다.


Fahrenheit 구조체를 기본 값을 사용하여 더 단순하게 만들 수 있다.

struct Fahrenheit {
    var temperature = 32.0
}


커스터마이징 초기화

입력 매개 변수와 옵셔널 프로퍼티 타입을 사용하거나, 초기화 중에 상수 프로퍼티를 할당하여 초기화 과정을 커스터마이징 할 수 있다.


초기화 매개 변수

초기화 정의의 일부분으로써 초기화 과정을 커스터마이징 하는 값의 이름과 타입을 정의하기 위해 초기화 매개 변수(initialization parameter)를 제공할 수 있다. 초기화 매개 변수는 함수/메소드 매개 변수 문법과 같은 기능을 가진다.

아래의 예시는 섭씨 온도를 저장하는 Celsius 구조체를 정의한다. Celsius 구조체는 두 개의 커스텀 이니셜라이저를 구현한다. init(fromFahrenheit:)init(fromKelvin:)이 그것이다.

struct Celsius {
    var temperatureInCelsius: Double
    init(fromFahrenheit fahrenheit: Double) {
        temperatureInCelsius = (fahrenheit - 32.0) / 1.8
    }
    init(fromKelvin kelvin: Double) {
        temperatureInCelsius = kelvin - 273.15
    }
}
let boilingPointOfWater = Celsius(fromFahrenheit: 212.0)
// boilingPointOfWater.temperatureInCelsius is 100.0
let freezingPointOfWater = Celsius(fromKelvin: 273.15)
// freezingPointOfWater.temperatureInCelsius is 0.0

첫 번째 이니셜라이저는 fromFahrenheit 인자 라벨이 붙은 매개 변수를 갖는다. 두 번째 이니셜라이저는 fromKelvin 인자 라벨을 갖는 매개 변수를 포함한다. 두 이니셜라이저는 하나의 인자를 일치하는 Celsius 값으로 변환하고, temperatureInCelsius 프로퍼티에 값을 저장한다.


프로퍼티 이름과 인자 라벨

함수와 메소드 매개 변수처럼 초기화 매개 변수도 이니셜라이저의 바디에서 사용하는 매개 변수와 이니셜라이저를 호출할 때 사용하는 인자 라벨을 가질 수 있다.

하지만 이니셜라이저는 함수나 메소드가 그러는 것처럼 함수 이름을 식별하지는 않는다. 이니셜라이저 매개 변수의 타입과 이름이 어떤 이니셜라이저가 호출되는지를 식별할 때 중요한 역할을 한다. 이 때문에 Swift는 인자 라벨을 붙이지 않은 모든 매개 변수에 자동으로 인자 라벨을 제공한다.

아래 예시는 세 상수 프로퍼티를 갖고 있는 Color 구조체를 정의한다. 각 프로퍼티는 0.0에서 1.0 사이의 값을 저장한다.

Color 프로퍼티는 Double 타입의 매개 변수 세 개(red, green, blue)가 있는 이니셜라이저와 하나의 매개 변수(white)를 갖는 이니셜라이저를 제공한다.

struct Color {
    let red, green, blue: Double
    init(red: Double, green: Double, blue: Double) {
        self.red   = red
        self.green = green
        self.blue  = blue
    }
    init(white: Double) {
        red   = white
        green = white
        blue  = white
    }
}

두 이니셜라이저는 각 이니셜라이저 매개 변수에 이름과 함께 값을 제공함으로써 새로운 Color 인스턴스를 생성할 수 있다.

let magenta = Color(red: 1.0, green: 0.0, blue: 1.0)
let halfGray = Color(white: 0.5)

인자 라벨 없이 이니셜라이저를 호출할 수는 없다. 인자 라벨은 반드시 이니셜라이저에서 사용되어야 한다. 생략할 경우 컴파일 시간에 에러가 발생한다.

let veryGreen = Color(0.0, 1.0, 0.0)
// this reports a compile-time error - argument labels are required

인자 라벨이 없는 이니셜라이저 매개 변수

만약 이니셜라이저 매개 변수에 인자 라벨을 사용하기 싫다면 언더스코어(_)를 인자 라벨 대신 사용할 수 있다.

struct Celsius {
    var temperatureInCelsius: Double
    init(fromFahrenheit fahrenheit: Double) {
        temperatureInCelsius = (fahrenheit - 32.0) / 1.8
    }
    init(fromKelvin kelvin: Double) {
        temperatureInCelsius = kelvin - 273.15
    }
    init(_ celsius: Double) {
        temperatureInCelsius = celsius
    }
}
let bodyTemperature = Celsius(37.0)
// bodyTemperature.temperatureInCelsius is 37.0

옵셔널 프로퍼티 타입

만약 커스텀 타입이 초기화 과정 중 할당되지 않을 수도 있는 값이어서, 혹은 나중에 값이 할당되기 때문에 "값이 없는" 상태를 허용한다면, 프로퍼티를 옵셔널 타입으로 선언한다. 옵셔널 타입의 프로퍼티는 자동적으로 nil 값으로 초기화된다.

아래 예제는 옵셔널 프로퍼티 response를 갖는 클래스 SurveyQuestion를 정의한다.

class SurveyQuestion {
    var text: String
    var response: String?
    init(text: String) {
        self.text = text
    }
    func ask() {
        print(text)
    }
}
let cheeseQuestion = SurveyQuestion(text: "Do you like cheese?")
cheeseQuestion.ask()
// Prints "Do you like cheese?"
cheeseQuestion.response = "Yes, I do like cheese."

초기화 중에 상수 프로퍼티를 할당하기

초기화할 때 상수 프로퍼티에 값을 할당할 수도 있다. 한번 상수 프로퍼티에 값을 할당하면, 더 이상 수정할 수 없다.

클래스 인스턴스에서 상수 프로퍼티는 그것을 소개하는 클래스가 초기화 중일 때만 수정할 수 있다. 자식 클래스에서는 수정할 수 없다.


위의 SurveyQuestion을 상수 프로퍼티를 사용하도록 변경한다. text 프로퍼티가 상수임에도 불구하고, 클래스의 이니셜라이저에서 수정할 수 있다.

class SurveyQuestion {
    let text: String
    var response: String?
    init(text: String) {
        self.text = text
    }
    func ask() {
        print(text)
    }
}
let beetsQuestion = SurveyQuestion(text: "How about beets?")
beetsQuestion.ask()
// Prints "How about beets?"
beetsQuestion.response = "I also like beets. (But not with cheese.)"


기본 이니셜라이저 (Default Initializer)

구조체나 클래스의 모든 프로퍼티가 기본 값을 갖고 있는 상황에서 이니셜라이저를 정의하지 않는다면, Swift는 기본 이니셜라이저(default initializer)를 제공한다. 이 이니셜라이저는 모든 프로퍼티를 기본 값으로 초기화한다.

이 예시는 쇼핑 리스트에 있는 물건의 이름, 수량, 구매 상태를 캡슐화 하는 ShoppingListItem 클래스를 정의한다.

class ShoppingListItem {
    var name: String?
    var quantity = 1
    var purchased = false
}
var item = ShoppingListItem()

모든 프로퍼티가 기본 값을 갖고 있으면서 부모 클래스가 없는 기반 클래스이기 때문에, ShoppingListItem 는 자동적으로 기본 이니셜라이저를 얻게 된다. name 프로퍼티는 옵셔널 문자열 프로퍼티여서 기본 값을 작성하지 않는 경우 nil이 된다. ShoppingListItem()과 같이 새로운 인스턴스를 생성할 때 기본 이니셜라이저가 사용된다.


구조체 타입을 위한 멤버 이니셜라이저

만약 구조체가 다른 커스텀 이니셜라이저를 정의하지 않는다면, 멤버 이니셜라이저(memberwise initializer)를 자동으로 얻는다. 기본 이니셜라이저와는 달리 구조체는 저장 프로퍼티가 기본 값을 갖지 않아도 멤버 이니셜라이저를 획득할 수 있다.

멤버 이니셜라이저는 새로운 구조체 인스턴스의 멤버 프로퍼티를 초기화하는 방법을 축약한 것이다. 새로운 인스턴스의 프로퍼티가 갖는 초기 값은 이름에 의해 멤버 이니셜라이저에 넣어질 수 있다.

아래 예시는 widthheight 두 프로퍼티를 갖는 Size 구조체를 정의한다. 두 프로퍼티는 기본 값 0.0을 할당 받는다. Size 프로퍼티는 자동적으로 멤버 이니셜라이저인 init(width:height:)를 얻게 된다.

struct Size {
    var width = 0.0, height = 0.0
}
let twoByTwo = Size(width: 2.0, height: 2.0)

멤버 이니셜라이저를 호출할 때, 기본 값을 갖는 프로퍼티의 값을 생략할 수 있다. 이니셜라이저는 초기화할 때 생략된 프로퍼티의 기본 값을 사용한다.

let zeroByTwo = Size(height: 2.0)
print(zeroByTwo.width, zeroByTwo.height)
// Prints "0.0 2.0"

let zeroByZero = Size()
print(zeroByZero.width, zeroByZero.height)
// Prints "0.0 0.0"


값 타입을 위한 이니셜라이저 위임

이니셜라이저는 인스턴스 초기화의 일부분을 수행하기 위해 다른 이니셜라이저를 호출할 수 있다. 이니셜라이저 위임(initializer delegation)이라 알려진 이 과정은 다수의 이니셜라이저 간에 코드의 중복을 피한다.

이니셜라이저 위임이 작동하는 방식에 대한 규칙과 어떤 위임 형태가 혀용되는지는 값 타입과 클래스 타입이 다르다. 값 타입(구조체와 열거형)은 상속을 지원하지 않고, 따라서 이니셜라이저 위임 과정은 상대적으로 간단하다. 자기 자신을 제공하는 다른 이니셜라이저에게만 위임할 수 있다. 반면 클래스는 다른 클래스로부터 상속을 받을 수 있다. 이는 클래스는 추가적인 책임을 갖고 있다는 것을 뜻한다. 상속한 모든 저장 프로퍼티가 초기화 과정에서 적당한 값을 할당받았는지 보장해야 한다.

값 타입은 커스텀 이니셜라이저를 작성할 때 self.init을 사용해 같은 값 타입으로부터의 다른 이니셜라이저를 참조할 수 있다. 이니셜라이저 안에서만 self.init을 호출할 수 있다.

만약 값 타입을 위한 커스텀 이니셜라이저를 정의한다면, 그 타입의 기본 이니셜라이저(또는 멤버 이니셜라이저)에 더 이상 접근할 수 없다. 이러한 제약은 더 복잡한 이니셜라이저에 의해 제공된 필수적인 추가 셋업이 자동으로 생성된 이니셜라이저를 사용하는 무언가에 의해 순환참조되는 것을 막는다.

만약 커스텀 값 타입을 기본 이니셜라이저, 멤버 이니셜라이저, 커스텀 이니셜라이저와 함께 초기화하고 싶다면, 값 타입의 원래 구현이 아닌 확장(extension)에서 커스텀 이니셜라이저를 작성하면 된다.


다음 예시는 기하학적 사각형을 표현하는 Rect 구조체를 정의한다. 이 예시는 SizePoint라는 두 가지 구조체를 필요로 한다.

struct Size {
    var width = 0.0, height = 0.0
}
struct Point {
    var x = 0.0, y = 0.0
}

Rect 구조체를 세 가지 방법으로 초기화할 수 있다.

  • 0으로 초기화된 originsize 프로퍼티 값을 사용.
  • 특정한 originsize 를 제공.
  • 특정한 center 포인트와 size를 제공

이러한 이니셜라이저 옵션들은 세 커스텀 이니셜라이저에 의해 표현된다.

struct Rect {
    var origin = Point()
    var size = Size()
    init() {}
    init(origin: Point, size: Size) {
        self.origin = origin
        self.size = size
    }
    init(center: Point, size: Size) {
        let originX = center.x - (size.width / 2)
        let originY = center.y - (size.height / 2)
        self.init(origin: Point(x: originX, y: originY), size: size)
    }
}

첫 번째 이니셜라이저인 init()은 기본 이니셜라이저와 같은 기능을 한다.

let basicRect = Rect()
// basicRect's origin is (0.0, 0.0) and its size is (0.0, 0.0)

두 번째 이니셜라이저인 `init(origin:size:)`은 멤버 이니셜라이저와 같은 기능을 한다.
let originRect = Rect(origin: Point(x: 2.0, y: 2.0),
                      size: Size(width: 5.0, height: 5.0))
// originRect's origin is (2.0, 2.0) and its size is (5.0, 5.0)

세 번째 이니셜라이저인 init(center:size:)은 좀 더 복잡하다. center 포인트와 size 값에 기반하여 원래 포인트를 계산한다. 그리고 init(origin:size) 이니셜라이저를 호출한다(혹은 위임한다).

let centerRect = Rect(center: Point(x: 4.0, y: 4.0),
                      size: Size(width: 3.0, height: 3.0))
// centerRect's origin is (2.5, 2.5) and its size is (3.0, 3.0)

init(center:size:) 이니셜라이저는 originsize의 새로운 값을 적당한 프로퍼티에 직접 할당할 수도 있다. 하지만 기존 이니셜라이저를 사용하는 게 더 편하고 명확히 의도를 드러낼 수 있다.



클래스 상속과 초기화

모든 클래스의 저장 프로퍼티는 반드시 초기화 과정에서 초기 값을 할당 받아야 한다. 부모 클래스에서 상속 받은 프로퍼티도 마찬가지다.

Swift는 클래스 타입을 위한 두 종류의 이니셜라이저를 정의한다. 모든 저장 프로퍼티가 초기 값을 받는 걸 보장하는 데 도움을 준다.


지정 이니셜라이저와 편리한 이니셜라이저 (Designed Initializers and Convenience Initializers)

지정 이니셜라이저(designed initializer)는 클래스의 주 이니셜라이저이다. 지정 이니셜라이저는 클래스의 모든 프로퍼티를 완전히 초기화 한다. 그리고 부모 클래스의 이니셜라이저를 호출하여 부모 클래스 연쇄를 통한 초기화 과정을 계속 진행한다.

클래스는 최소한의 지정 이니셜라이저를 갖도록 의도된다. 일반적으로 하나의 지정 이니셜라이저를 갖는다. 지정 이니셜라이저는 초기화가 발생하고 부모 클래스 체인을 계속하는 "깔대기" 포인트와 같다.

모든 클래스는 적어도 하나의 지정 이니셜라이저를 가져야 한다. 부모 클래스로부터 하나 이상의 지정 이니셜라이저를 상속받아야 하는 경우도 있다.

편리한 이니셜라이저(convenience initializer)는 클래스의 이니셜라이저를 지원하는 부 이니셜라이저다. 같은 클래스로부터 지정 이니셜라이저를 호출하기 위해 편리한 이니셜라이저를 정의할 수 있다. 편리한 이니셜라이저로 지정 이니셜라이저의 매개변수에 기본 값을 설정한다.

만약 클래스에서 필요하지 않다면 편리한 이니셜라이저를 만들 필요가 없다. 공통적인 초기화 패턴에 대한 축약이 시간을 절약하거나 클래스 초기화의 의도를 더 명확하게 만들 때마다 편리한 이니셜라이저를 만들면 된다.


지정 이니셜라이저와 편리한 이니셜라이저 문법

지정 이니셜라이저는 값 타입의 이니셜라이저와 같은 방식으로 쓰여진다.

init(parameters) {
    statements
}

편리한 이니셜라이저는 같은 스타일이지만, convenience 키워드를 init 앞에 붙인다.

convenience init(parameters) {
    statements
}

클래스 타입을 위한 이니셜라이저 위임

지정 이니셜라이저와 편리한 이니셜라이저의 관계를 간단히 하기 위해, Swift는 이니셜라이저 간 위임 호출이 일어날 때 다음 세 가지 규칙을 적용한다.

  1. 지정 이니셜라이저는 직계 부모 클래스의 지정 이니셜라이저를 반드시 호출해야 한다.
  2. 편리한 이니셜라이저는 같은 클래스의 다른 이니셜라이저를 반드시 호출해야 한다.
  3. 편리한 이니셜라이저는 궁극적으로 지정 이니셜라이저를 반드시 호출해야 한다.

간단히 말하자면 이렇다.

  • 지정 이니셜라이저는 반드시 부모 클래스를 위임한다.
  • 편리한 이니셜라이저는 반드시 같은 레벨을 위임한다.

이미지로 표현하면 다음과 같다.

initializerDelegation01_2x

이 규칙은 클래스의 사용자가 각 클래스의 인스턴스를 만드는 방법에는 아무 영향을 끼치지 않는다. 클래스 이니셜라이저의 구현을 어떻게 작성할 것인지에만 영향을 미친다.


2단계 초기화 (Two-Phase Initialization)

Swift에서 클래스 초기화는 2단계로 진행된다. 첫번째 단계에서는 각 저장 프로퍼티는 클래스에서 정한 초기값으로 초기화 된다. 모든 저장 프로퍼티의 상태가 결정되면 두번째 단계가 시작된다. 두번째 단계에서는 새로운 인스턴스의 사용이 준비됐다고 알려주기 전에 저장 프로퍼티를 커스터마이징하는 단계이다.

2단계 초기화의 사용은 클래스 상속에서 각 클래스에 완전한 융통성을 주면서도 초기화를 안전하게 만들어 준다. 2단계 초기화는 초기화되기 전에 값에 접근되는 것을 방지한다. 예상치 못한 다른 이니셜라이저에 의해 다른 값이 프로퍼티 값에 설정되는 것을 막기도 한다.

Swift의 2단계 초기화는 Objective-C에서의 초기화와 유사하다. 주된 차이점은 첫번째 단계다. Objective-C에서는 모든 프로퍼티에 0 혹은 nil 값을 할당한다. Swift의 초기화는 좀 더 유연해서 커스텀한 초기 값을 할당 할 수 있다. 그리고 0nil이 잘못된 초기값으로 지정된 경우 대처할 수 있다.

Swift의 컴파일러는 2단계 초기화가 에러없이 끝나는 것을 보장하기 위해 4단계 안전 확인(safety-check)을 한다.


안전 확인 1단계

지정 이니셜라이저는 클래스의 모든 프로퍼티가 부모 클래스의 이니셜라이저에 위임하기 전에 초기화됐음을 보장해야 한다. 객체의 메모리는 모든 저장 프로퍼티의 초기 상태가 확인돼야만 완전히 초기화된 것으로 간주한다. 이러한 규칙을 만족시키기 위해 지정 이니셜라이저 반드시 그것의 모든 다른 이니셜라이저로 넘기기 전에 프로퍼티가 초기화됐는지 확인해야 한다.

안전 확인 2단계

지정 이니셜라이저는 반드시 상속한 프로퍼티에 값을 할당하기 전에 부모 클래스의 이니셜라이저로 위임해야 한다. 그렇지 않으면 지정 이니셜라이저가 할당한 새로운 값은 부모 클래스에 의해 덮어 쓰여진다.

안전 확인 3단계

편리한 이니셜라이저는 어떤 프로퍼티에 값을 할당하기 전에 반드시 다른 이니셜라이저에 위임해야 한다. 그렇지 않으면 편리한 이니셜라이저가 할당한 새로운 값은 클래스의 지정 이니셜라이저에 의해 덮어 쓰여진다.

안전 확인 4단계

이니셜라이저는 초기화의 1단계가 끝나기 전까지 다른 인스턴스 메소드를 호출하거나, 인스턴스 프로퍼티의 값을 읽거나, 값으로써 self를 참조할 수 없다.

클래스 인스턴스는 1단계가 끝나기 전까지는 완전히 유효하지 않다. 1단계 종료 시점부터 프로퍼티에 접근하거나, 메소드를 호출할 수 있고, 클래스 인스턴스가 유효해진다.


4단계 안전 확인에 기반하여 2단계 초기화는 다음과 같이 작동한다.

1단계

  • 지정/편리한 이니셜라이저가 클래스에서 호출된다.
  • 클래스의 새로운 인스턴스를 위한 메모리가 할당된다. 이 메모리는 아직 초기화되지 않았다.
  • 지정 이니셜라이저는 모든 저장 프로퍼티가 값을 갖고 있는지 확인한다. 이 저장 프로퍼티들의 메모리는 아직 초기화되지 않았다.
  • 지정 이니셜라이저는 부모 클래스의 이니셜라이저로 넘기고, 같은 작업을 실시한다.
  • 최상단 클래스에 도착할 때까지 클래스 상속 연쇄를 계속 진행한다.
  • 최상단 클래스에 도착하고, 연쇄의 마지막 클래스가 모든 저장 프로퍼티의 값 보유를 보장한다면, 인스턴스의 메모리는 완전히 초기화된 것으로 간주한다. 1단계가 완료된다.

2단계

  • 최상단 클래스부터 밑으로 작업을 실시한다. 각 지정 이니셜라이저는 인스턴스를 커스터마이징 할 옵션을 갖고 있다. 이니셜라이저는 이제 self에 접근 가능하며, 그것의 프로퍼티를 수정하고 인스턴스 메소드를 호출할 수 있다.
  • 최종적으로 연쇄 안의 모든 편리한 이니셜라이저는 인스턴스를 커스터마이징 하고 self를 사용할 수 있는 옵션을 갖게 된다.

2단계 초기화를 이미지로 나타내면 다음과 같다.

twoPhaseInitialization01_2x twoPhaseInitialization02_2x

이니셜라이저 상속과 오버라이딩

Objective-C의 자식 클래스와는 달리, Swift의 자식 클래스는 부모 클래스의 이니셜라이저를 기본 값으로 상속하지 않는다. Swift의 접근은 부모 클래스의 간단한 이니셜라이저가 더 복잡한 자식 클래스에 상속되어서, 자식 클래스의 새로운 인스턴스가 완전히 초기화되지 않은 상태로 생성되는 것을 막는다.

부모 클래스의 이니셜라이저는 안전하고 적절한 특정 상황에서는 상속된다.


만약 부모 클래스와 동일한 이니셜라이저를 자식 클래스에서 만들고자 한다면, 자식 클래스에서 이런 이니셜라이저의 커스텀 구현을 제공할 수 있다.

부모 클래스의 지정 이니셜라이저와 매치되는 자식 클래스 이니셜라이저를 만들 때, 지정 이니셜라이저를 오버라이드 할 수 있다. 따라서 자식 클래스의 이니셜라이저를 정의할 때 override 키워드를 붙여야 한다. 자동으로 제공된 기본 이니셜라이저를 오버라이딩 할 때도 그래야 한다.

오버라이드된 프로퍼티, 메소드, 서브스크립트처럼 override 키워드의 존재는 Swift에게 부모 클래스가 매치되는 지정 니니셜라이저를 갖고 있는지, 오버라이딩 하는 이니셜라이저의 매개 변수는 유효한지 확인하게 한다.

부모 클래스의 지정 이니셜라이저를 오버라이딩 할 때는 자식 클래스에서 구현하는 이니셜라이저가 편리한 이니셜라이저이더라도 항상 override 키워드를 작성해야 한다.


거꾸로, 만약 부모 클래스의 편리한 이니셜라이저와 매치되는 자식 클래스의 이니셜라이저를 작성한다면, 부모 클래스의 편리한 이니셜라이저는 위에서 언급한 이니셜라이저 위임 규칙 때문에 자식 클래스에 의해 절대 직접적으로 호출되지 않는다. 그러므로, 자식 클래스는 부모 클래스 이니셜라이저를 오버라이딩 할 수 없다. 결과적으로, 부모 클래스의 편리한 이니셜라이저와 매치되는 이니셜라이저를 구현할 때는 override 키워드를 쓰면 안 된다.


아래 예시는 Vehicle 기반 클래스를 정의한다. 이 기반 클래스는 기본 값이 0인 numberOfWheels 저장 프로퍼티를 선언한다. numberOfWheels 프로퍼티는 계산 프로피티인 description에서 차량의 특성을 묘사하기 위해 사용된다.

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

Vehicle 클래스는 저장 프로퍼티에만 기본 값을 제공하고, 다른 커스텀 이니셜라이저가 없다. 그 결과 기본 이니셜라이저가 자동으로 생성된다. 기본 이니셜라이저는 항상 지정 이니셜라이저이고, numberOfWheels가 0인 새로운 Vehicle 인스턴스를 만드는 데 사용할 수 있다.

let vehicle = Vehicle()
print("Vehicle: \(vehicle.description)")
// Vehicle: 0 wheel(s)

Vehicle을 상속한 Bicycle을 정의한다.

class Bicycle: Vehicle {
    override init() {
        super.init()
        numberOfWheels = 2
    }
}

Bicycle 자식 클래스는 커스텀 지정 연산자인 init()을 정의한다. 이 지정 이니셜라이저는 부모 클래스의 지정 이니셜라이저와 매치되고, Bicycle 버전임을 표시하기 위해 override 키워드를 붙인다.

Bicycleinit() 이니셜라이저는 super.init()을 통해 Bicycle의 부모 클래스로부터 기본 클래스를 호출한다. 이는 상속한 numberOfWheels 프로퍼티가 Vehicle에서 초기화되었음을 보장한다.

Bicycle의 인스턴스를 만든다면 상속된 계산 프로퍼티인 description을 호출할 수 있다.

let bicycle = Bicycle()
print("Bicycle: \(bicycle.description)")
// Bicycle: 2 wheel(s)

만약 자식 클래스의 이니셜라이저가 2번째 초기화 단계에서 아무런 커스터마이징도 하지 않고, 부모 클래스가 인자 없는 이니셜라이저를 갖고 있다면, super.init()을 생략할 수 있다.


이 예시에선 Vehicle의 자식 클래스인 Hoverboard를 정의한다. 이니셜라이저에서 Hoverboard 클래스는 color 프로퍼티에만 값을 할당한다. 명시적으로 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)"
    }
}

Hoverboard의 인스턴스는 Vehicle 이니셜라이저에서 지원되는 numberOfWheels 기본 값을 사용한다.

let hoverboard = Hoverboard(color: "silver")
print("Hoverboard: \(hoverboard.description)")
// Hoverboard: 0 wheel(s) in a beautiful silver

자식 클래스는 초기화 도중 상속한 변수 프로퍼티를 수정할 수 있지만, 상수 프로퍼티를 수정할 수는 없다.


자동 이니셜라이저 상속

위에서 언급한 것처럼, 자식 클래스는 부모 클래스의 이니셜라이저를 기본으로 상속하지는 않는다. 하지만 특정한 조건을 충족한다면 부모 클래스의 이니셜라이저가 자동으로 상속된다. 사실 많은 상황에서 직접 이니셜라이저를 오버라이드 할 필요는 없다. 그렇게 하는 게 안전할 때만 최소한의 노력으로 부모 클래스 이니셜라이저를 상속할 수 있다.

자식 클래스에서 추가한 모든 프로퍼티에 기본 값을 제공하면 다음 두 가지 규칙이 적용된다.

규칙 1

만약 자식 클래스가 지정 이니셜라이저를 정의하지 않았다면, 자동으로 부모 클래스의 모든 지정 이니셜라이저를 상속한다.

규칙 2

만약 자식 클래스가 부모 클래스의 모든 지정 이니셜라이저를 구현한다면, 자동으로 부모 클래스의 편리한 이니셜라이저를 상속한다.

규칙 2에 따라 자식 클래스는 부모 클래스의 지정 이니셜라이저를 자식 클래스의 편리한 이니셜라이저로 구현할 수 있다.


지정/편리한 이니셜라이저의 사용

다음 예시는 지정 이니셜라이저, 편리한 이니셜라이저, 자동으로 상속된 이니셜라이저를 보여 준다. Food, RecipeIngredient, ShoppingListItem 클래스의 계층과 이니셜라이저가 상호작용하는 방식을 정의한다.


Food는 계층의 기반 클래스이다.

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

클래스는 기본 멤버 이니셜라이저를 갖고 있지 않으며, Food 클래스는 하나의 인자(name)을 갖는 지정 이니셜라이저를 제공한다.

let namedMeat = Food(name: "Bacon")
// namedMeat's name is "Bacon"

새로운 Food 인스턴스의 모든 저장 프로퍼티가 완전히 초기화되는 것을 보장하기 때문에 init(name: String) 이니셜라이저는 지정 이니셜라이저가 된다. Food 클래스는 부모 클래스가 없기 때문에 super.init()을 호출할 필요가 없다.

Food 클래스는 인자가 없는 편리한 이니셜라이저인 init()을 제공한다. init() 이니셜라이저는 init(name: String)[Unnamed]name 값으로 하여 위임한다.

let mysteryMeat = Food()
// mysteryMeat's name is "[Unnamed]"

계층의 두 번째 클래스는 Food의 자식 클래스인 RecipeIngredient다. 이 클래스는 quantity 프로퍼티를 선언한다. 그리고 두 개의 이니셜라이저를 정의한다.

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)
    }
}
initializersExample02_2x

RecipeIngredient은 하나의 지정 이니셜라이저인 init(name: String, quantity: Int)을 갖는다. 모든 프로퍼티를 채우는 데 사용된다. 이 이니셜라이저는 quantity 인자를 quantity 프로퍼티에 할당한다. 그런 뒤에 Food 클래스의 init(name: String)에 이니셜라이저를 위임한다. 이 과정은 2단계 초기화의 안전 확인 1단계를 만족한다.

RecipeIngredient은 편리한 이니셜라이저인 init(name: String)도 정의한다. 이 편리한 이니셜라이저는 명시적 quantity가 없이 생성된 모든 RecipeIngredient의 인스턴스는 quantity가 1이라고 가정한다. 편리한 이니셜러이저는 RecipeIngredient의 인스턴스를 더 빠르고 편리하게 만들 수 있게 한다. 그리고 quantity가 1인 RecipeIngredient 인스턴스를 여러 개 만들 때 코드의 중복을 피할 수 있다.

init(name: String) 편리한 이니셜라이저는 Foodinit(name: String) 지정 이니셜라이저와 같은 매개 변수를 취한다. 이 편리한 이니셜라이저가 부모 클래스의 지정 이니셜라이저를 오버라이드 하기 때문에, override 키워드를 표시해야만 한다.

RecipeIngredientinit(name: String)가 편리한 이니셜라이저이긴 하지만, RecipeIngredient는 부모 클래스의 모든 이니셜라이저를 구현하고 있다. 따라서 RecipeIngredient는 부모 클래스의 편리한 이니셜라이저 역시 자동으로 상속할 수 있다. 따라서 Food의 편리한 이니셜라이저인 init() 역시 사용 가능하다.

let oneMysteryItem = RecipeIngredient()
let oneBacon = RecipeIngredient(name: "Bacon")
let sixEggs = RecipeIngredient(name: "Eggs", quantity: 6)

계층의 세 번째이자 마지막 클래스는 RecipeIngredient의 자식 클래스인 ShoppingListItem이다.

쇼핑 리스트의 모든 물건은 "구매되지 않은" 상태로 시작한다. ShoppingListItemBoolean 프로퍼티인 purchased를 선언한다. 프로퍼티의 기본 값은 false다.

class ShoppingListItem: RecipeIngredient {
    var purchased = false
    var description: String {
        var output = "\(quantity) x \(name)"
        output += purchased ? " ✔" : " ✘"
        return output
    }
}

ShoppingListItempurchased에 초기 값을 지정하기 위한 이니셜라이저를 정의하지 않는다. 쇼핑 리스트의 모든 물건은 구매하지 않은 상태로 시작하기 때문이다.

initializersExample03_2x

새로운 ShoppingListItem 인스턴스를 만들기 위해 세 가지 상속된 이니셜라이저를 사용할 수 있다.

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 ✘


실패 가능한 이니셜라이저

실패할 수 있는 이니셜라이저를 사용하는 게 유용할 때가 있다. 이 실패는 유효하지 않은 매개 변수, 요구되는 외부 자원의 부재, 또는 초기화 성공을 막는 다른 조건에 의해 발생한다.

실패할 수 있는 초기화 조건에 대처하기 위해 하나 이상의 실패 가능한 이니셜라이저를 정의한다. init 키워드 뒤에 물음표를 붙임으로써 실패 가능한 이니셜라이저를 작성한다(init?).

실패 가능한 이니셜라이저와 일반 이니셜라이즈를 같은 매개 변수 타입과 이름을 갖도록 정의할 수는 없다.


실패 가능한 이니셜라이저는 초기화한 타입의 옵셔널 값을 생성한다. 실패 가능한 이니셜라이저 안에 return nil을 작성함으로써 초기화 실패가 발생할 수 있음을 표시한다.

엄밀히 말하면 이니셜라이저는 값을 반환하지 않는다. self가 초기화가 끝나는 시점에 완전히 정확하게 초기화되었음을 보장하는 것이 이니셜라이저의 역할이다. 초기화 실패를 유발하기 위해 return nil을 작성하긴 하지만, 초기화 성공을 알리기 위해 return 키워드를 사용하지는 않는다.


아래 예시에서, 숫자 타입 변환을 위한 실패 가능한 이니셜라이저가 구현된다. 숫자 타입 간의 변환이 값을 정확하게 유지함을 보장하기 위해, init(exactly:) 이니셜라이저를 사용한다. 타입 변환이 값을 유지하지 못하면 이니셜라이저는 실패한다.

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 does not maintain value")
}
// Prints "3.14159 conversion to Int does not maintain value"

아래의 예시는 species 문자열 상수 프로퍼티를 갖는 Animal 구조체를 정의한다. Animal 구조체는 species 매개 변수 하나를 갖는 실패 가능한 이니셜라이저를 정의한다. 이 이니셜라이저는 이니셜라이저에 들어온 species 값이 비어 있는지 확인한다. 만약 빈 문자열이 들어오면 초기화는 실패한다.

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

실패 가능한 이니셜라이저를 새로운 Animal 인스턴스의 초기화를 시도하여 성공했는지 확인하는 데 사용할 수 있다.

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 could not be initialized")
}
// Prints "The anonymous creature could not be initialized"

빈 문자열 값을 확인하는 것과 nil을 확인하는 것은 다른 문제다. 위 예시에서, 빈 문자열("")은 유효한 비옵셔널 문자열이다. 하지만 species 프로퍼티의 값으로는 적절하지 않다.


열거형의 실패 가능한 이니셜라이저

열거형에도 실패 가능한 이니셜라이저를 사용할 수 있다. 이니셜라이저는 매개 변수가 적절한 열거형 케이스와 매칭되지 않을 경우 실패할 수 있다.

아래 예시는 TemperatureUnit 열거형을 정의한다. 적절한 Character 열거형 케이스를 찾기 위해 실패 가능한 이니셜라이저를 사용한다.

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 is not a defined temperature unit, so initialization failed.")
}
// Prints "This is not a defined temperature unit, so initialization failed."

Raw 값을 사용하는 열거형의 실패 가능한 이니셜라이저

raw 값을 사용하는 열거형은 자동으로 실패 가능한 이니셜라이저 init?(rawValue:)를 얻는다. 이 이니셜라이저는 적절한 raw 값 타입의 rawValue 매개 변수를 가지며, 매칭되는 열거형 케이스를 찾는다. 찾지 못할 경우 실패를 발생시킨다.

TemperatureUnitCharacter 타입의 raw 값을 사용하도록 수정할 수 있다.

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 is not a defined temperature unit, so initialization failed.")
}
// Prints "This is not a defined temperature unit, so initialization failed."

초기화 실패의 전파

클래스, 구조체, 열거형의 실패 가능한 이니셜라이저는 같은 클래스, 구조체, 열거형의 다른 실패 가능한 이니셜라이저에 위임할 수 있다. 비슷하게, 자식 클래스의 실패 가능한 이니셜라이저는 부모 클래스의 실패 가능한 이니셜라이저에 위임할 수 있다.

두 경우 모두 초기화 실패를 유발하는 다른 이니셜라이저에 위임할 경우 전체 초기화 과정은 즉시 실패한다. 더이상 초기화 코드가 실행되지 않는다.

실패 가능한 이니셜라이저는 일반 이니셜라이저에 위임할 수 있다. 잠재적인 실패 상황을 실패가 없는 기존 초기화 과정에 더하고자 할 경우 사용한다.


아래 예시는 Product의 자식 클래스인 CartItem을 정의한다. CartItem 클래스는 온라인 쇼핑 카트 속 아이템을 모델로 한다. CartItem은 저장 상수 프로퍼티인 quantity를 가지며, 이 프로퍼티는 항상 적어도 1의 값을 갖고 있음을 보장한다.

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)
    }
}

실패 가능한 이니셜라이저는 quantity 값이 1 이상인지 확인한다. 만약 quantity가 유효하지 않다면 전체 초기화 과정은 즉시 실패하고 더 이상의 코드 진행은 이루어지지 않는다. 비슷하게, Product의 실패 가능한 이니셜라이저는 name이 빈 문자열인지 확인한다.

만약 비어 있지 않은 문자열 name과 1 이상의 quantityCartItem 인스턴스를 생성한다면, 초기화는 성공한다.

if let twoSocks = CartItem(name: "sock", quantity: 2) {
    print("Item: \(twoSocks.name), quantity: \(twoSocks.quantity)")
}
// Prints "Item: sock, quantity: 2"

quantity 값이 0인 CartItem 인스턴스를 만들면 CartItem 이니셜라이저는 실패를 유발한다.

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"

name이 비어 있는 문자열인 CartItem 인스턴스를 만들면 Product 이니셜라이저가 실패를 유발한다.

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"

실패 가능한 이니셜라이저 오버라이드 하기

자식 클래스에서 실패 가능한 이니셜라이저를 오버라이드 할 수 있다. 주모 클래스의 실패 가능한 이니셜라이저를 자식 클래스의 일반 이니셜라이저로 오버라이드 할 수도 있다. 이것은 초기화를 부모클래스가 초기화 실패를 허용함에도 불구하고 실패하지 않는 자식 클래스를 정의할 수 있게 한다.

부모 클래스의 실패 가능한 이니셜라이저를 자식 클래스의 일반 이니셜라이저로 오버라이드 한다면, 부모 클래스의 이니셜라이저로 위임하는 방법은 실패 가능한 부모 클래스 이니셜라이저의 결과를 강제로 푸는(force-unwrap) 것이다.

실패 가능한 이니셜라이저를 일반 이니셜라이저로 오버라이드 할 수 있지만, 반대는 불가능하다.


아래 예시는 Document 클래스를 정의한다. 이 클래스는 비어 있지 않은 문자열 값이나 nil이 될 수도 있는 name 프로퍼티를 갖는다. 빈 문자열은 허용하지 않는다.

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
    }
}

다음 예시는 Document의 자식 클래스인 AutomaticallyNamedDocument를 정의한다. AutomaticallyNamedDocumentDocument의 두 지정 이니셜라이저를 오버라이드 한다. 이 오버라이드들은 AutomaticallyNamedDocument 인스턴스가 만약 이름이 없이 초기화될 경우 [Untitled]name 값으로 가지며, 빈 문자열은 init(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
        }
    }
}

AutomaticallyNamedDocument는 부모 클래스의 init?(name:)을 일반 이니셜라이저인 init(name:)으로 오버라이드 한다. AutomaticallyNamedDocument 빈 문자열이 들어왔을 때 부모 클래스와 다르게 대처함으로써 이니셜라이저는 실패할 필요가 없어졌다.


자식 클래스의 이니셜라이저를 구현하면서 부모 클래스의 실패 가능한 이니셜라이저를 호출할 때 강제로 푸는 것이 가능하다. 예를 들어 UntitledDocument 자식 클래스의 name은 항상 [Untitled]이 된다.

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

만약 부모 클래스의 init(name:) 이니셜라이저가 name이 빈 문자열로 호출되면, 강제 언래핑 연산자가 런타임 에러를 발생시킨다. 그러나 문자열 상수로 호출되어 이니셜라이저가 실패하지 않으므로, 이 경우에는 런타임 오류가 발생하지 않는다.


Init! 실패 가능한 이니셜라이저

암시적으로 풀린(implicitly unwrapped) 옵셔널 인스턴스를 생성하는 실패 가능한 이니셜라이저를 정의할 수 있다. init 키워드 뒤에 물음표 대신 느낌표를 붙인다.

init?init!으로 위임하거나, init?init!으로 오버라이드 할 수 있다. 반대의 경우도 가능하다. init에서 init!으로 위임할 수도 있다. init! 이니셜라이저는 초기화에 실패할 경우 assertion을 일으킨다.



요구된 이니셜라이저 (Required Initializers)

required 키워드를 클래스 이니셜라이저 선언 앞에 작성하여 모든 자식 클래스는 이 이니셜라이저를 반드시 구현해야 함을 알릴 수 있다.

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

자식 클래스에서 요구된 이니셜라이저를 구현할 때 반드시 required 키워드를 붙여야 한다. 요구된 지정 이니셜라이저를 오버라이드 할 때는 override를 작성하지 않는다.

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
    }()
}

클로저의 중괄호 끝에 빈 소괄호 쌍이 따라오는 것을 볼 수 있다. 이는 Swift가 클로저를 즉시 실행한다는 것을 뜻한다. 만약 소괄호를 생략한다면 클로저의 반환 값이 아니라 클로저 자신을 프로퍼티에 할당해야 한다.

만약 프로퍼티를 초기화하는 데 클로저를 사용한다면, 클로저가 실행되는 시점에서 인스턴스의 나머지 부분은 아직 초기화가 되지 않았다는 것을 기억해야 한다. 클로저 안에서 다른 프로퍼티에 접근할 수 없다는 것을 뜻한다. 프로퍼티가 기본 값을 갖고 있더라도 사용할 수 없다. self 프로퍼티를 사용하거나 다른 메소드를 호출할 수도 없다.


아래 예시는 Chessboard 구조체를 정의한다. 체스는 8 x 8 보드에서 플레이되며, 검은색과 하얀색 정사각형으로 구분된다. Chessboard 구조체는 길이가 64인 Bool 배열 boardColors 프로퍼티를 갖는다. true는 검은 사각형을, false는 하얀 사각형을 나타낸다. 배열의 첫 번째 아이템은 좌상단 모서리이며, 마지막 아이템은 우하단 모서리이다.

boardColors 배열은 클로저를 사용해 초기화된다.

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"

새로운 Chessboard 인스턴스가 만들어질 때마다 클로저가 실행되고, boardColors의 기본 값이 계산되어 반환된다.

0개의 댓글