본문 바로가기
프로그래밍/Swift

[Swift] Initialization

by 별준 2022. 1. 3.

References

Contents

  • Setting Initial Values for Stored Properties
  • Customizing Initialization
  • Default Initializers
  • Initializer Delegation for Value Types
  • Class Inheritance and Initialization
  • Failable Initializers
  • Required Initializers
  • Setting a Default Property Value with a Closure or Function

Initialization(초기화)는 클래스, 구조체, 열거형의 인스턴스를 사용하기 위해 준비하는 프로세스입니다. 이 프로세스에는 인스턴스의 각 저장 속성(stored property)의 초기값을 세팅하고, 새로운 인스턴스를 사용하기 전에 필요한 다른 셋업이나 초기화를 수행합니다.

 

초기화 프로세스는 initializers(이니셜라이저)를 정의하여 구현할 수 있는데, 이는 특정 타입의 새로운 인스턴스를 만들 때 호출하는 특별한 메소드와 같습니다. Objective-C의 이니셜라이저와는 달리, Swift의 이니셜라이저는 값을 반환하지 않습니다. 이니셜라이저의 주요 역할은 새로운 인스턴스를 처음 사용하기 전에 올바르게 초기화하는 것입니다.

 

클래스 타입의 인스턴스는 또한 deinitializer를 구현할 수 있는데, 이는 클래스의 인스턴스의 할당을 해제하기 전에 커스텀 cleanup을 수행합니다. deinitializer는 다음 포스팅에서 자세히 살펴보겠습니다.

 

초기화에 관련된 내용들이 조금 많습니다.. !


Setting Initial Values for Stored Properties

클래스나 구조체는 그 인스턴스가 생성될 때, 반드시 모든 저장 속성에 적절한 초기값을 설정해주어야 합니다. 저장 속성은 초기화되지 않은 상태로 둘 수 없습니다.

저장 속성의 초기값은 이니셜라이저 내에서 설정하거나, 속성의 정의 부분에서 기본 속성 값을 할당함으로써 설정할 수 있습니다. 아래에서 자세하게 살펴보겠습니다.

기본값을 저장 속성에 할당하거나, 이니셜라이저 내에서 초기값을 설정할 때, 속성의 값은 즉시 설정되고, 어떠한 옵저버 속성도 호출하지 않습니다.

 

Initializers

이니셜라이저는 특정 타입의 인스턴스를 생성할 때 호출됩니다. 간단한 형태로, 이니셜라이저는 파라미터가 없는 인스턴스 메소드와 같으며, init 키워드를 사용하여 작성합니다.

init() {
    // perform some initialization here
}

아래 예제 코드는 Fahrenheit라는 구조체를 정의하는데, 이 구조체는 화씨 단위로 표현되는 온도를 저장합니다. Fahrenheit 구조체는 temperature라는 하나의 저장 속성을 가지고 있으며, 타입은 Double 입니다.

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으로 초기화합니다.

 

Default Property Values

저장 속성의 초기값은 위에서 살펴본 것처럼 이니셜라이저 내에서 설정할 수 있지만, 속성의 정의하는 부분에서 기본 속성 값(Default Property Value)을 지정하여 초기화를 할 수도 있습니다. 기본 속성 값은 속성을 정의할 때 초기값을 할당함으로써 설정할 수 있습니다.

속성이 항상 같은 초기값을 사용하는 경우 이니셜라이저 내에서 초기값을 설정하는 것보다 기본값을 설정하는 것이 좋습니다. 물론 결과는 동일하지만, 기본값은 속성의 초기화를 선언의 일부로 취급합니다. 이는 더 짧고 명확한 이니셜라이저이며 기본값을 통해 속성의 타입을 추론할 수 있도록 합니다. 또한, 기본값을 사용하면 default initializer와 initializer inheritance(상속)을 더 쉽게 사용할 수 있습니다.

위에서 살펴본 Fahrenheit 구조체는 temperature 속성을 선언할 때 기본값을 할당하여 아래와 같이 더 간단하게 초기화를 수행할 수 있습니다.

struct Fahrenheit {
    var temperature = 32.0
}

 


Customizing Initialization

Initialization Parameters

이니셜라이저의 정의 부분에서 초기화 파라미터(initialization parameters)를 전달하여 초기화 프로세스를 커스터마이징할 수 있습니다. 초기화 파라미터는 함수와 메소드의 파라미터와 동일하며, 동일한 문법으로 작성됩니다.

 

아래 예제 코드는 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라는 인자가 제공되며, 파라미터의 이름은 fahrenheit입니다. 두 번째 이니셜라이저는 fromKelvin이라는 인자를 가지고 있으며, 파라미터 이름은 kelvin입니다. 두 이니셜라이저는 전달받은 인자를 대응되는 화씨값으로 변환하고 변환된 값을 temperatureInCelsius에 저장합니다.

 

Parameter Names and Arguments Labels

함수와 메소드의 파라미터와 같이, 초기화 파라미터도 이니셜라이저 바디에서 사용하는 파라미터 이름(parameter name)과 이니셜라이저를 호출할 때 사용되는 인자 라벨(argument label)을 갖습니다.

그러나, 이니셜라이저는 함수와 메소드처럼 괄호 앞에 식별할 수 있는 함수 이름이 없습니다. 따라서 이니셜라이저의 파라미터 이름과 타입은 호출해야하는 이니셜라이저를 구분하는데 중요한 역할을 합니다. 이러한 이유로 만약 argument label이 제공되지 않을 경우에 Swift는 자동으로 모든 파라미터의 argument label을 제공합니다.

 

아래 예제 코드는 Color라는 구조체를 정의하고 있습니다. 이 구조체는 red,green,blue라는 3개의 상수 속성을 정의합니다. 이 속성들은 0.0과 1.0 사이의 값을 저장하며, 이 값은 색상에서 red, green, blue의 양을 나타냅니다.

Color는 Double 타입인 3개의 파라미터를 가진 이니셜라이저를 제공합니다. Color는 또한 하나의 white 파라미터를 가진 두 번째 이니셜라이저도 제공하며, 이는 3가지 색상 요소를 모두 같은 값으로 초기화합니다.

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)

 

argument label을 사용하지 않고 이니셜라이저를 호출하는 것은 불가능합니다. Argument label은 정의된 경우 항상 이니셜라이저에서 사용되어야 하며, 이를 생략하는 것은 컴파일 에러를 발생시킵니다.

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

 

Initializer Parameters Without Argument Labels

만약 이니셜라이저 파라미터에 argument label을 사용하고 싶지 않다면, underscore(_)를 argument label 대신 사용하면 됩니다.

아래는 위에서 구현한 Celsius 구조체의 확장된 버전인데, 이미 섭씨 단위인 Double 값으로부터 새로운 Celsius 인스턴스를 생성하는 이니셜라이저가 추가되었습니다.

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

 

 

Optional Property Types

optional 타입의 속성을 선언하여 커스텀 타입이 "no value"라는 값을 가지는 저장 속성을 가지도록 할 수 있습니다. optional 타입의 속성은 자동으로 nil로 초기화되며, 이는 초기화 중에 해당 속성이 "no value yet"이라는 것을 의미합니다.

 

아래 예제 코드는 optional String 속성인 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."

질문에 대한 응답은 질문을 하기 전까지 알 수 없습니다. 따라서, response 속성의 String? 타입으로 선언됩니다. 이는 자동으로 nil로 초기화됩니다.

 

Assigning Constant Properties During Initialization

상수 속성은 초기화 중 어느 지점에서도 값이 할당될 수 있습니다만, 상수 속성에 값이 한 번 할당되면 더 이상 수정할 수 없습니다.

클래스 인스턴스의 경우에 상수 속성은 해당 속성을 정의(도입)한 클래스에 의해서만 초기화 중에 수정할 수 있습니다. 서브클래스에서는 수정할 수 없습니다.

 

위에서 살펴본 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 Initializers

Swift는 모든 속성에 기본값을 제공하고 적어도 하나의 이니셜라이저도 제공하지 않는 구조체나 클래스에 default 이니셜라이저를 제공합니다. default 이니셜라이저는 모든 속성이 기본값으로 설정된 새로운 인스턴스를 생성합니다.

 

아래 예제 코드는 ShoppingListItem이라는 클래스를 정의합니다.

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

ShoppingListItem의 모든 속성이 기본값을 가지고 있고, 수퍼클래스가 아닌 베이스클래스이기 때문에 ShoppingListItem은 자동으로 기본 이니셜라이저를 제공합니다(name 속성은 optional String 속성인데, 이는 자동으로 nil로 기본값을 갖습니다).

 

Memberwise Initializers for Structure Types

구조체 타입은 어떠한 커스텀 이니셜라이저도 정의되지 않은 경우에 자동으로 memberwise 이니셜라이저를 제공합니다. default 이니셜라이저와는 다르게 구조체는 기본값이 없는 저장 속성이 있더라도 memberwise 이니셜라이저를 제공합니다.

memberwise 이니셜라이저는 새로운 구조체 인스턴스의 멤버 속성들을 초기화하는 간단한 방법입니다. 새로운 인스턴스의 속성들의 초기값은 속성 이름으로 memberwise 이니셜라이저로 전달되기만하면 됩니다.

 

아래 예제 코드는 width와 height 속성을 갖고 있는 Size라는 구조체를 정의합니다. Size 구조체는 자동으로 init(width:height:) memberwise 이니셜라이저를 생성하며 새로운 Size 인스턴스를 생성하는데 사용할 수 있습니다.

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

memberwise 이니셜라이저를 호출할 때, 기본값을 있는 속성은 생략할 수 있습니다. 위 예제에서 Size 구조체에서 height와 width는 둘 다 기본값을 가지고 있습니다. 따라서, 아래와 같이 해당 속성들을 생략하여 초기화할 수 있는데, 생략하게 된다면 기본값으로 초기화됩니다.

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 for Value Types

이니셜라이저에서 다른 이니셜라이저를 호출할 수 있는데, 이 프로세스를 initializer delegation이라고 합니다. 이는 여러 이니셜라이저를 사용할 때 코드의 중복을 피하기 위해서 사용됩니다.

 

Initializer delegation의 동작 방식과 허용되는 delegation 형식은 value types과 class types에 따라 다릅니다. Value type(구조체와 열거형)에서는 상속이 지원되지 않으므로 직접 제공하는 이니셜라이저에 대해서만 delegation이 가능하므로 Value type에서의 initializer delegation은 비교적 단순합니다.

그러나 클래스는 다른 클래스로부터 상속받을 수 있습니다. 즉, 클래스는 초기화 중에 상속되는 모든 저장 속성에 적절한 값이 할당되도록 해야합니다. 이에 관한 자세한 내용은 아래에서 설명하도록 하겠습니다.

 

Value type에서 self.init을 사용하여 다른 이니셜라이저를 참조할 수 있도록 할 수 있습니다. self.init은 이니셜라이저 내부에서만 호출할 수 있습니다.

 

value type의 커스텀 이니셜라이저를 정의한다면, 더 이상 해당 타입에서 default 이니셜라이저(or memberwise 이니셜라이저)는 액세스할 수 없습니다. 이는 실수로 자동으로 생성되는 이니셜라이저를 사용하는 상황을 방지합니다.

만약 커스텀 이니셜라이저와 default/memberwise 이니셜라이저를 같이 사용하고 싶다면, extension을 사용하여 커스텀 이니셜라이저를 작성하면 됩니다. 이는 다른 포스팅에서 다루도록 하겠습니다.

 

아래 예제 코드는 사각형을 표현하는 Rect 구조체를 정의합니다. 이 예제에서는 Size와 Point라는 두 개의 구조체가 사용되며, 두 구조체에서의 모든 속성은 0.0을 기본값으로 갖습니다.

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

Rect 구조체는 3가지 방법으로 초기화할 수 있습니다.

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

 

첫 번째 Rect 이니셜라이저인 init()은 커스텀 이니셜라이저가 제공되지 않았을 때 자동으로 생성되는 default 이니셜라이저와 동작이 같습니다. 이 이니셜라이저를 호출하는 것은 origin과 size 속성이 Point(x: 0.0, y:0.0), Size(width:0.0, height:0.0)으로 초기화되는 Rect 인스턴스를 반환합니다.

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

 

두 번째 Rect 이니셜라이저인 init(origin:size:)는 memberwise 이니셜라이저와 기능이 동일합니다. 이 이니셜라이저는 간단히 origin과 size argument 값을 저장 속성에 할당합니다.

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)

 

세 번째 Rect 이니셜라이저인 init(center:size:)는 다소 복잡할 수 있습니다. 이는 center Point와 size 값을 통해 원점을 계산하고, init(origin:size:) 이니셜라이저를 호출(delegates)하여 새로운 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)

 


Class Inheritance and Initialization

모든 클래스의 저장 속성(수퍼클래스에서 상속받은 속성 포함)은 반드시 초기화 중에 초기값이 할당되어야 합니다.

Swift는 클래스 타입에서 모든 저장 속성이 초기값을 갖도록 하는 두 종류의 이니셜라이저를 정의합니다.

designated initializer와 convenience initializer입니다.

 

Designated Initializers and Convenience Initializers

Designated initializers는 클래스의 기본 이니셜라이저입니다. designated 이니셜라이저는 클래스에서 정의된 모든 속성을 완전히 초기화하고, 적절한 수퍼클래스의 이니셜라이저를 호출하여 수퍼클래스와 연관된 초기화 작업을 수행합니다.

 

클래스에서 designated 이니셜라이저는 거의 없고, 하나만 가지고 있는 것이 매우 일반적입니다. designated 이니셜라이저는 초기화 프로세스가 수퍼클래스로 이어지게 해주는 'funnel' 포인트라고 할 수 있습니다.

모든 클래스는 적어도 하나의 designated 이니셜라이저를 가져야합니다. 몇몇 클래스에서 이 요구사항은 수퍼클래스로부터 하나 또는 그 이상의 designated 이니셜라이저를 상속받아서 만족될 수 있습니다. 아래의 Automatic Initializer Inheritance에서 다시 설명하겠습니다.

 

Convenience initializers는 클래스에서 지원하는 두 번째 이니셜라이저입니다. Convenience initializers는 같은 클래스로부터 designated 이니셜라이저를 호출하여 정의할 수 있는데, designated 이니셜라이저의 일부 파라미터에 기본값이 설정된 것이 convenience 이니셜라이저라고 생각하면 될 듯합니다.

또한, 해당 클래스의 인스턴스를 특정 use 케이스나 특정 input value 타입에 대해 생성하는 convenience 이니셜라이저를 정의할 수 있습니다.

필요가 없다면 convenience 이니셜라이저는 정의되지 않아도 됩니다.

 

Syntax for Designated and Convenience Initializers

Designated 이니셜라이저는 아래와 같은 방법으로 작성됩니다.

init(parameters) {
    statements
}

 

Convenience 이니셜라이저도 비슷하지만, convenience modifier를 init 키워드 앞에 붙여서 작성합니다.

convenience init(parameters) {
    statements
}

 

Initializer Delegation for Class Types

designated와 convenience 이니셜라이저 간의 관계를 간단하게 하기 위해, Swift는 이니셜라이저 간의 delegation 호출을 위한 3가지 규칙을 적용합니다.

  • Rule 1: designated 이니셜라이저는 해당 클래스가 직접 상속받는 수퍼클래스의 designated 이니셜라이저를 호출해야한다.
  • Rule 2: convenience 이니셜라이저는 같은 클래스의 다른 이니셜라이저만 호출해야 한다.
  • Rule 3: convenience 이니셜라이저는 궁긍적으로 designated 이니셜라이저를 호출해야 한다.

이를 요약하면 간단하게,

  1. Designated 이니셜라이저는 반드시 수퍼클래스를 delegation해야 하고,
  2. Convenience 이니셜라이저는 반드시 같은 레벨에서 delegation해야 합니다.

으로 정리할 수 있습니다.

 

이 규칙은 아래 그림에서 쉽게 설명해주고 있습니다.

위 그림에서 서브클래스의 convenience 이니셜라이저는 같은 클래스(레벨)에서 다른 desinated 이니셜라이저를 호출하고, designated 이니셜라이저는 수퍼클래스의 designated 이니셜라이저를 호출합니다.

수퍼클래스에서 한 convenience 이니셜라이저는 다른 convenience 이니셜라이저를 호출하고 이는 designated 이니셜라이저를 호출합니다.

(위와 같은 형태를 꼭 따라할 필요는 없고, 규칙만 지켜주면 됩니다.)

 

다음은 조금 더 복잡한 형태의 초기화 delegation입니다.

 

Two-Phase Initialization

Swift에서 클래스 초기화는 2단계로 이루어집니다. 첫 번째 단계에서 각 저장 속성은 초기값으로 초기화됩니다. 그리고 모든 저장 속성의 초기 상태가 결정되면, 두 번째 단계가 시작되는데, 두 번째 단계는 각 클래스가 사용 준비가 완료되기 전에 저장 속성을 커스터마이징하는 단계입니다.

 

2단계의 초기화 과정은 초기화를 safe하게 해주면서, 클래스 계층 구조에서 각 클래스에 완전한 flexibility를 제공합니다. 2단계의 초기화는 속성 값들이 초기화전에 액세스되는 것을 막아주고, 속성 값이 예기치 않게 다른 이니셜라이저에 의해서 다른 값으로 설정되는 것을 막아줍니다.

Swift의 컴파일러는 2단계 초기화를 에러없이 완료하기 위해서 도움이 되는 4가지 safety-check를 수행합니다.

 

  • Safety check 1: designated 이니셜라이저는 수퍼클래스의 이니셜라이저에 delegate하기 전에 클래스의 모든 속성을 초기화해야 합니다. 객체의 메모리는 모든 저장 속성이 초기 상태를 갖추어야 완전히 초기화된 것으로 간주됩니다. 따라서, 이 조건을 만족하기 위해서 designated 이니셜라이저는 반드시 다른 이니셜라이저로 넘기기 전에 소유하는 모든 속성을 초기화해야 합니다.
  • Safety check 2: designated 이니셜라이저는 상속된 속성을 할당하기 전에 반드시 수퍼클래스 이니셜라이저로 delegate해야 합니다. 만약 그렇지 않는다면, designated 이니셜라이저가 할당한 새로운 값은 수퍼클래스의 이니셜라이저에 의해서 덮어씌워지게 됩니다.
  • Safety check 3: convenience 이니셜라이저는 어떤 속성의 값을 할당하기 전에 다른 이니셜라이저로 delegate해야 합니다. 만약 그렇지 않는다면, convenience 이니셜라이저가 할당한 새로운 값은 그 클래스의 designated 이니셜라이저에 의해서 덮어씌워지게 됩니다.
  • Safety check 4: 이니셜라이저는 1단계 초기화가 끝나기 전에 self의 값을 참조하거나 어떤 인스턴스 속성에 액세스하거나 메소드를 호출할 수 없습니다.

 

다음은 2단계 초기화가 위의 4가지 safety check를 기반으로 어떻게 동작하는지 보여줍니다.

- Phase 1

  • designated or convenience 이니셜라이저가 클래스에서 호출됨
  • 그 클래스의 새로운 인스턴스를 위한 메모리가 할당되고, 아직 메모리는 초기화되지 않음
  • 해당 클래스의 designated 이니셜라이저는 모든 저장 속성이 기본값을 가지고 있는지 확인. 이때, 이 저장 속성들의 메모리는 초기화됨
  • designated 이니셜라이저는 수퍼클래스 이니셜라이저가 수퍼클래스의 저장 속성에 대해 동일한 작업을 수행하도록 함
  • 상속 chain의 top에 도달할 때까지 계속 수행됨
  • Top까지 도달하고, final 클래스가 모든 저장 속성의 값을 가지면, 인스턴스의 메모리는 완전히 초기화된 것으로 간주되고, 1단계가 완료됨

- Phase 2

  • Top에서 부터 내려가면서, 각 designated 이니셜라이저는 인스턴스를 커스터마이징할 수 있음. 이니셜라이저는 이제 self에 액세스할 수 있고, 속성들을 수정하거나 인스턴스 메소드를 호출할 수 있음
  • 마지막으로 convenience 이니셜라이저는 self를 사용하여 인스턴스를 커스터마이즈할 수 있음

 

아래 이미지는 가상의 서브클래스와 수퍼클래스에서의 초기화 호출을 보여줍니다.

여기서, 서브클래스의 convenience 이니셜라이저의 호출로 초기화가 시작됩니다. 이 convenience 이니셜라이저는 아직 어떠한 속성도 수정할 수 없습니다. 이는 같은 클래스의 designated 이니셜라이저에게 delegate 합니다.

designated 이니셜라이저는 서브클래스의 모든 속성이 값을 가지는지 확인(safety check 1)합니다. 그리고 수퍼클래스의 designated 이니셜라이저를 호출하여 연결된 초기화를 계속 수행합니다.

수퍼클래스의 designated 이니셜라이저는 수퍼클래스의 모든 속성이 값을 가지는지 확인합니다. 더 이상 초기화할 수퍼클래스들이 없다면 더 이상의 delegation은 없습니다.

수퍼클래스의 모든 속성들이 초기값이 가지면, 메모리는 완전히 초기화된 것으로 간주되고 1단계 초기화가 완료됩니다.

 

다음 그림은 동일한 초기화 호출에서의 2단계 초기화를 보여줍니다.

수퍼클래스의 designated 이니셜라이저는 이제 인스턴스를 커스터마이즈할 기회를 가집니다(할 필요가 없더라도). 수퍼클래스의 designated 이니셜라이저가 끝나면, 서브클래스의 designated 이니셜라이저가 추가적인 커스터마이즈를 수행합니다. 마지막으로, 서브클래스의 designated 이니셜라이저가 끝나면, 처음 호출되었던 convenience 이니셜라이저가 추가 커스터마이즈를 수행합니다.

 

Initializer Inheritance and Overriding

Objective-C와는 달리 Swift의 서브클래스들은 수퍼클래스의 이니셜라이저를 기본적으로 상속하지 않습니다. 이는 무분별한 이니셜라이저 초기화로부터 서브클래스의 인스턴스들이 잘못 초기화되는 것을 막기 위함입니다.

수퍼클래스의 이니셜라이저는 안전하거나 적절한 특정 상황에서 상속됩니다. 자세한 내용은 아래 Automatic Initializer Inheritance에서 설명하도록 하겠습니다.

만약 커스텀 서브클래스가 수퍼클래스와 동일한 이니셜라이저 중의 하나 이상을 사용하도록 하려면 서브클래스에서 해당 이니셜라이저의 커스텀 구현을 제공하면 됩니다.

 

수퍼클래스의 designated 이니셜라이저와 일치하는 서브클래스의 이니셜라이저를 작성할 때, 이는 designated 이니셜라이저의 override를 제공하는 것과 같습니다. 그러므로 서브클래스의 이니셜라이저 정의에서 override modifier를 추가해주어야 합니다. 자동으로 생성되는 default 이니셜라이저를 오버라이드할 때에도 마찬가지입니다.

 

오버라이드된 속성, 메소드, 서브스크립트처럼 override modifier의 존재는 Swift가 수퍼클래스에서 일치하는 designated 이니셜라이저가 있는지 체크하도록 합니다.

반대로 만약 수퍼클래스의 convenience 이니셜라이저와 일치하는 서브클래스의 이니셜라이저를 작성한다면, 위에서 설명한 Initializer Delegation for Class Types의 규칙에 따라 해당 수퍼클래스의 convenience 이니셜라이저는 서브 클래스에서 직접 호출할 수 없습니다. 따라서 엄밀히 말하면 서브 클래스는 수퍼클래스의 이니셜라이저의 오버라이드를 제공하지 않습니다. 결과적으로 수퍼클래스의 convenience 이니셜라이저와 일치하는 이니셜라이저를 구현할 때에는 override modifier가 필요없습니다.

 

아래 예제 코드는 Vehicle이라는 base 클래스를 정의하고 있습니다. 이 클래스는 numberOfWheels라는 저장 속성을 선언하고 Int 타입의 기본값 0으로 설정됩니다. 이 속성은 클래스의 description이라는 연산 속성에 의해서 사용되며, 현재 탈 것의 특징을 설명하는 String 설명을 생성합니다.

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

Vehicle 클래스는 오직 저장 속성에 대한 기본값만을 제공하고, 자체 커스텀 이니셜라이저는 없습니다. 따라서, 자동으로 default 이니셜라이저가 생성됩니다. default 이니셜라이저는 항상 designated 이니셜라이저이고, 새로운 Vehicle 인스턴스를 생성할 때 사용됩니다.

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

 

다음 코드는 Vehicle의 서브클래스인 Bicycle 입니다.

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

Bicycle 서브클래스는 커스텀 designated 이니셜라이저 init()을 정의합니다. 이 designated 이니셜라이저는 Bicycle의 수퍼클래스의 designated 이니셜라이저와 일치하고, 따라서 Bicycle 버전의 이니셜라이저는 override가 추가되어 있습니다.

Bicycle의 init() 이니셜라이저는 먼저 super.init()을 호출하며, 이는 Bicycle 클래스의 수퍼클래스의 default 이니셜라이저를 호출합니다. 이는 상속받은 numberOfWheels 속성을 Bicycle이 수정할 기회를 얻기 전에 Vehicle에서 초기화하도록 합니다. super.init()이 호출된 이후에 numberOfWheels 값은 새로운 값인 2로 변경됩니다.

 

Bicycle의 인스턴스를 새롭게 만들면, 상속받은 description 연산 속성을 호출해 numberOfWheels의 값이 업데이트된 것을 확인할 수 있습니다.

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

만약 서브클래스 이니셜라이저가 2단계의 초기화에서 어떠한 커스터마이즈도 수행하지 않고, 수퍼클래스는 argument가 없는 designated 이니셜라이저를 갖는다면, 서브클래스의 모든 저장 속성의 값을 할당한 후에 super.init() 호출을 생략할 수 있습니다. (수퍼클래스에서 상속받은 속성을 수정하려면 생략하면 안됩니다.)

 

아래는 Hoverboard라는 Vehicle의 다른 서브클래스를 정의하고 있습니다. 이 서브클래스의 이니셜라이저에서 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 이니셜라이저에서 제공되는 기본 바퀴 개수를 사용합니다.

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

 

Automatic Initializer Inheritance

위에서 언급했듯이, 서브클래스는 기본적으로 수퍼클래스의 이니셜라이저를 상속받지 않습니다. 그러나 수퍼클래스 이니셜라이저는 특정 상황이 만족되면 자동으로 상속됩니다. 실제로 많은 일반적인 상황에서 이니셜라이저 오버라이드를 작성할 필요가 없으며, 안전하다면 언제든지 수퍼클래스의 이니셜라이저를 상속받을 수 있습니다.

 

서브클래스에서 새로 추가한 모든 속성에 기본값이 지정되면, 다음의 두 가지 규칙이 적용됩니다.

  • Rule 1: 만약 서브클래스가 어떠한 designated 이니셜라이저도 정의하지 않는다면, 자동으로 수퍼클래스의 모든 designated 이니셜라이저는 상속된다.
  • Rule 2: 서브클래스가 수퍼클래스의 모든 designated 이니셜라이저를 구현한 경우(or Rule 1), 수퍼클래스의 모든 convenience 이니셜라이저는 상속된다.

이 규칙들은 서브클래스에 추가 convenience 이니셜라이저를 추가하는 경우에도 적용됩니다.

 

Designated and Convenience Initializers in Action

아래 예제를 통해 designated, convenience 이니셜라이저와 automatic 이니셜라이저 상속이 어떻게 쓰이는지 살펴보겠습니다. 이 예제에서는 Food, RecipeIngredient, ShoppingListItem이라는 3개의 클래스 계층 구조를 정의하며, 그 이니셜라이저들이 어떻게 상호작용하는지 설명합니다.

 

이 계층 구조의 base 클래스는 Food이며, 음식의 이름을 갖고 있는 간단한 클래스입니다. Food 클래스는 하나의 String 속성을 가지고 있으며 2개의 이니셜라이저가 있습니다.

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

아래 그림은 Food 클래스의 초기화 chain을 보여줍니다.

클래스들은 default memberwise 이니셜라이저가 없습니다. 그래서 Food 클래스는 name이라는 하나의 argument를 취하는 designated 이니셜라이저를 제공합니다. 이 이니셜라이저는 지정된 이름의 새로운 Food 인스턴스를 생성하는데 사용됩니다.

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

 

두 번째 클래스는 Food의 서브클래스인 RecipeIngredient 입니다. 이 클래스는 요리 레시피의 재료를 모델링합니다. quantity라는 Int 속성을 가지고 있고, 인스턴스 생성을 위한 두 개의 이니셜라이저를 정의합니다.

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

아래 그림은 RecipeIngredient 클래스의 이니셜라이저 chain을 보여줍니다.

RecipeIngredient 클래스는 하나의 designated 이니셜라이저, init(name: String, quantity: Int)를 가지고 있습니다. 이 이니셜라이저는 전달받은 quantity argument의 값을 quantity 속성에 할당합니다. 그리고 이니셜라이저는 Food 클래스의 init(name: String) 이니셜라이저로 delegate 합니다. (이 프로세스는 safety check 1을 만족합니다.)

 

RecipeIngredient는 또한 convenience 이니셜라이저, init(name: String)을 정의합니다. 이 convenience 이니셜라이저는 RecipeIngredient 인스턴스의 quantity를 1로 가정하고 인스턴스를 생성합니다. 이 이니셜라이저의 정의는 RecipeIngredient 인스턴스를 더 빠르고 편리하게 생성할 수 있도록 하며, 불필요한 코드 중복을 방지합니다.

 

init(name: String)의 convenience 이니셜라이저는 Food의 init(name: String) designated 이니셜라이저와 동일한 파라미터를 취합니다. 이 convenience 이니셜라이저는 수퍼클래스의 designated 이니셜라이저를 오버라이드하고 있으므로, 반드시 override modifier가 있어야합니다.

비록 RecipeIngredient는 init(name: String)을 convenience 이니셜라이저로 제공하지만, 그럼에도 RecipeIngredient는 수퍼클래스의 모든 designated 이니셜라이저 구현을 제공합니다. 즉, RecipeIngredient는 자동으로 수퍼클래스의 모든 convenience 이니셜라이저를 상속합니다.

 

이 예제에서, RecipeIngredient의 수퍼클래스는 Food이고, Food 클래스는 init()이라는 하나의 convenience 이니셜라이저를 가지고 있습니다. 그러므로 이 이니셜라이저는 RecipeIngredient에 상속됩니다. 상속된 버전의 init() 함수는 Food 버전이 아닌 RecipeIngredient 버전의 init(name: String)에 delegate한다는 것을 제외하고는 Food 버전과 완전히 같은 방식으로 동작합니다.

 

아래의 3가지 초기화는 새로운 RecipeIngredient 인스턴스를 생성하는데 사용할 수 있습니다.

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

첫 번째 아무런 인자가 없는 초기화는 RecipeIngredient의 convenience 이니셜라이저를 호출합니다.

 

 

계층 구조에서 세 번째인 마지막 클래스는 RecipeIngredient의 서브클래스인 ShoppingListItem 입니다. 이 클래스 쇼핑리스트에서의 레시피 재료를 모델링합니다.

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

모든 속성에 기본값을 정의하고 있으므로, 자체 이니셜라이저는 정의하지 않고 있습니다. 따라서 ShoppingListItem은 자동으로 수퍼클래스의 모든 designated와 convenience 이니셜라이저를 상속받습니다.

 

그림으로 표현하면 다음과 같습니다.

ShoppingListItem의 인스턴스는 상속받은 3개의 이니셜라이저를 사용하여 생성할 수 있습니다.

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 ✘

 


Failable Initializers

클래스, 구조체, 열거형에서 실패할 수 있는 초기화를 정의할 수 있습니다. 유효하지 않은 초기화 파라미터 값이나, 필요한 외부 리소스가 없거나 초기화를 성공할 수 없는 다른 조건들로 인하여 초기화에 실패할 수 있습니다.

 

초기화 실패에 대응하기 위해서는 클래스, 구조체, 열거형을 정의할 때 하나 이상의 failable 이니셜라이저를 정의해야합니다. Failable 이니셜라이저는 init 키워드 뒤에 물음표(?)를 붙여서 정의할 수 있습니다.

같은 파라미터 타입과 이름을 갖는 failable 이니셜라이저와 nonfailable 이니셜라이저를 정의할 수 없습니다. 두 이니셜라이저는 파라미터로 구분되기 때문에 동시에 사용할 수 없습니다.

Failable 이니셜라이저는 초기화하는 타입의 optional value를 생성합니다. failable 이니셜라이저 내에서 return nil을 작성해서 초기화 실패했다는 것을 트리거할 수 있습니다.

조금 더 자세히 말하자면, 사실 이니셜라이저는 값을 반환하지 않습니다. 이니셜라이저의 역할은 초기화가 끝날 때 완전하게 정확하게 초기화되도록 하는 것입니다. return nil이 초기화 실패를 트리거하지만, 초기화 성공을 위한 return 키워드는 사용하지 않습니다.

 

예를 들어, failable 이니셜라이저는 숫자 타입 변환에서 구현됩니다. 숫자 타입간 변화를 통해 값을 정확하게 유지하려면, init(exactly:) 이니셜라이저를 사용하면 됩니다. 만약 타입 변환이 값을 유지할 수 없다면, 이니셜라이저는 실패합니다. 다음 코드는 failable 이니셜라이저 Int(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 doesn't maintain value")
}
// Prints "3.14159 conversion to Int doesn't maintain value"

 

아래 예제 코드는 species라는 String 속성을 포함하는 Animal 구조체를 정의하고 있습니다. Animal 구조체는 failable 이니셜라이저를 정의합니다. 이 이니셜라이저는 이니셜라이저로 전달되는 species 값이 빈 문자열인지 체크하고, 빈 문자열이라면 초기화 실패가 트리거됩니다. species 속성의 값이 설정되면, 초기화는 성공입니다.

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

아래는 새로운 Animal 인스턴스 초기화를 failable 이니셜라이저를 사용한 예제 코드 입니다.

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"

만약 failable 이니셜라이저의 species 파라미터에 빈 문자열을 전달하면 초기화 실패가 트리거 됩니다.

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"

 

Failable Initializers for Enumerations

하나 이상의 파라미터에서 적절한 열거형 케이스를 선택하는 failable 이니셜라이저를 사용할 수 있습니다. 이 이니셜라이저는 만약 제공된 파라미터로 적절한 열거형 케이스를 선택할 수 없다면 실패합니다.

 

아래 예제 코드는 TemperatureUnit 열거형을 정의합니다. 여기에 가능한 3개의 상태(Kelvin, celsius, fahrenheit)가 있습니다. Failable 이니셜라이저는 온도 심볼을 표현하는 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 isn't a defined temperature unit, so initialization failed.")
}
// Prints "This isn't a defined temperature unit, so initialization failed."

 

Failable Initializers for Enumerations with Raw Values

raw 값을 가지는 열거형은 자동으로 failable initializer, init?(rawValue:)를 생성합니다. 이 이니셜라이저는 rawValue라는 파라미터를 전달받고, 일치하는 열거형 케이스를 선택하는데, 만약 매칭되는 값이 존재하지 않는다면 초기화 실패를 트리거합니다.

 

아래 코드는 위에서 살펴본 TemperatureUnit 열거형을 Character 타입의 raw 값을 가지도록 수정했습니다. 따라서 아래 코드에서는 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."

 

Propagation of Initialization Failure

클래스, 구조체, 열거형의 Failable 이니셜라이저는 동일한 클래스, 구조체, 열거형의 다른 failable 이니셜라이저로 delegate할 수 있습니다. 마찬가지로 서브클래스의 failable 이니셜라이저도 수퍼클래스의 failable 이니셜라이저로 delegate할 수 있습니다.

 

두 경우에서, 만약 초기화 실패를 야기하는 다른 이니셜라이저로 delegate한다면, 전체 초기화 프로세스는 즉시 실패하고 더 이상 초기화 코드는 실행되지 않습니다.

failable 이니셜라이저는 nonfailable 이니셜라이저에게 delegate할 수도 있습니다.

다음 예제 코드는 CartItem이라는 Product의 서브클래스를 정의하고 있습니다. 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)
    }
}

CartItem의 failable 이니셜라이저는 quantity 값이 1 이상인지 체크합니다. 만약 1 이상이 아니라면 전체 초기화 프로세스는 즉시 실패하고, 초기화 코드는 더 이상 수행되지 않습니다. 마찬가지로, Product의 failable 이니셜라이저는 name 값을 체크하는데, 만약 name이 빈 문자열이라면 초기화 프로세스는 즉시 실패합니다.

nonempty name과 1 이상의 quantity를 가지는 CartItem 인스턴스를 생성한다면, 초기화는 성공합니다.

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

유효하지 않는 값을 갖는 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"

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"

 

Overriding a Failable Initializer

다른 이니셜라이저처럼 서브클래스에서 수퍼클래스의 failable 이니셜라이저를 override할 수 있습니다. 또한, 서브클래스의 nonfailable 이니셜라이저가 수퍼클래스의 failable 이니셜라이저를 override할 수도 있습니다. 이는 비록 수퍼클래스의 초기화가 실패할 수 있더라도, 서브클래스에서의 초기화 실패는 일어날 수 없는 서브클래스를 정의할 수 있다는 의미입니다. (반대로 failable 이니셜라이저가 nonfailable 이니셜라이저를 override하는 것은 불가능합니다.)

 

아래 예제 코드는 Document라는 클래스를 정의하고 있습니다. 이 클래스는 초기화할 때, name 값으로 nonempty 문자열이나 nil의 값을 가질 수 있고, 빈 문자열은 될 수 없습니다.

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를 정의합니다. 이 서브클래스는 Document의 designated 이니셜라이저 두 개를 모두 override합니다. 따라서, 만약 이 서브클래스의 인스턴스가 name없이 초기화되거나 init(name:) 이니셜라이저에 빈 문자열이 전달되면 name의 초기값이 "[Untitled]"인 인스턴스가 생성됩니다.

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는 수퍼클래스의 failable 이니셜라이저인 init?(name:)을 nonfailable인 init(name:) 이니셜라이저가 override하고 있습니다. AutomaticallyNamedDocument는 빈 문자열 케이스를 수퍼클래스와 다른 방법으로 처리하고 있기 때문에, 서브클래스의 이니셜라이저는 실패하지 않습니다.

 

이니셜라이저에서 수퍼클래스의 failable 이니셜라이저를 서브클래스의 nonfailable 이니셜라이저에서 호출하여 forced unwrapping을 사용할 수 있습니다. 예를 들어, 아래의 UntitledDocument 서브클래스는 항상 "[Untitled]"로 명명되고, 초기화동안 수퍼클래스의 failable init(name:) 이니셜라이저를 사용합니다.

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

위 코드에서는 UntitleDocument 이니셜라이저에서 Document의 기본 이니셜라이저를 override했고, 그 내부에서 수퍼클래스의 failable 이니셜라이저를 호출하는데, 그 값이 옵셔널 값을 갖지 않도록 느낌표를 사용하여 forced unwrapping하였습니다.

 

The init! Failable Initializer

일반적으로 init 키워드 뒤에 물음표를 붙여 적절한 타입의 optional 인스턴스를 생성하는 failable 이니셜라이저를 정의합니다. 또는 적절한 타입의 언랩한 optional 인스턴스를 생성하는 failable 이니셜라이저를 정의할 수 있습니다. 이는 물음표 대신 init 키워드 뒤에 느낌표(!)를 붙여서 정의할 수 있습니다.

 

init?으로부터 init!으로 delegate할 수 있고, 그 반대도 가능합니다. 그리고 init!으로 init?을 override할 수 있고, 그 반대도 물론 가능합니다. 또한, 비록 init! 이니셜라이저가 초기화 실패를 야기한다면 assertion을 트리거하지만, init으로부터 init!으로 delegate할 수 있습니다.

 


Required Initializers

클래스의 서브클래스가 해당 이니셜라이저를 필수로 구현하도록 하려면 required modifier를 클래스 이니셜라이저 앞에 붙여줍니다.

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

이러한 required 이니셜라이저를 상속받은 서브클래스에서도 반드시 required를 추가하여 다른 서브클래스에게도 이 이니셜라이저는 required 이니셜라이저라는 것을 알려주어야 합니다.

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

 


Setting a Default Property Value with a Closure or Function

만약 저장 속성의 기본값이 다른 커스터마이즈 또는 설정이 필요로 한다면, 그 속성의 커스터마이즈 기본값을 제공하는 클로저(closure) 또는 전역 함수(global function)를 사용할 수 있습니다. 

 

이러한 클로저나 함수는 일반적으로 속성과 동일한 타입의 임시 변수를 생성하고, 원하는 초기 상태를 갖도록 한다음 그 임시 값을 반환합니다.

다음 코드는 클로저를 사용하여 기본값을 제공하는 방법을 보여줍니다.

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

someProperty는 클로저가 실행된 후 반환 타입이 someType인 someValue를 기본값으로 갖게 됩니다.

속성을 초기화할 때 클로저를 사용한다면, 인스턴스의 다른 속성들은 클로저가 수행되는 시점에서 초기화가 완료된 것이 아닙니다. 따라서, 클로저 내에서 다른 속성의 값에 액세스할 수 없습니다. 물론 self도 사용할 수 없고, 다른 인스턴스 메소드도 호출할 수 없습니다.

 

아래 예제 코드는 체스 게임의 보드를 모델링하는 Chessboard 구조체를 정의합니다. 8x8 보드이며, 검정색과 하얀색 사각형이 번갈아 나옵니다.

게임 보드를 표현하기 위해서 Chessboard 구조체는 boardColors라는 하나의 속성을 갖는데, 이 속성은 크기가 64인 Bool 배열입니다. 배열에서 true 값은 검정색 사각형을 표현하고, false는 하얀색 사각형을 표현합니다.

boardColor 배열은 클로저를 통해 초기화됩니다.

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

 

새로운 Chessboard 인스턴스가 생성될 때, 클로저가 실행되고 boardColors의 기본값이 반환됩니다. 위 예제에서의 클로저는 보드의 각 사각형에 적절한 색을 설정하여 임시 배열인 temporaryBoard에 저장합니다. 그리고 셋업이 완료되면 이 값을 반환합니다. 반환된 값은 boardColors에 저장됩니다.

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

 

'프로그래밍 > Swift' 카테고리의 다른 글

[Swift] Optional Chaining  (0) 2022.01.08
[Swift] Deinitialization  (0) 2022.01.06
[Swift] Inheritance (상속)  (0) 2021.12.30
[Swift] Subscripts (서브스크립트)  (0) 2021.12.30
[Swift] Methods  (0) 2021.12.26

댓글