[최종 수정일 : 2018.08.31]

원문 : 애플 개발 문서 Swift 4.2 Language Guide - Initialization

초기화(Initialization)

초기화(Initialization)은 클래스, 구조체, 열거형의 인스턴스를 사용하기 전에 준비하는 과정입니다. 이러한 과정은 인스턴스에서 각 저장 프로퍼티에 초기 값을 설정하는 것을 포함하고 새 인스턴스를 사용할 준비가 되기 전에 필요한 다른 설정이나 초기화를 수행합니다.

특정 타입의 새로운 인스턴스를 생성하기 위해 특별한 메소트처럼 호출될수 있는 초기화(initializers)를 정의하여 초기화과정을 구현합니다. Objective-C 초기화는 다르게, Swift 초기화는 값을 반환하지 않습니다. 그것들의 주요 역할은 타입의 새로운 인스턴스가 처음 사용되기 전에 확실히 초기화되도록 하는 것입니다.

클래스 타입의 인스턴스는 클래스의 인스턴스가 메모리에서 해제되기 전에 사용자가 지정한 정리를 수행하는 해제(deinitializer)를 구현할수 있습니다. 해제(deinitializer)에 대한 자세한 정보는 해제(Deinitialization)를 보세요.

저장 프로퍼티에 대해 초기값 설정하기(Setting Initial Values for Stored Properties)

클래스와 구조체는 클래스나 구조체의 인스턴스가 생성될때까지 반드시(must) 저장 프로퍼티 모두 적절한 초기값이 설정되야 합니다. 저장 프로퍼티는 불확실한 상태로 남아있을수 없습니다.

초기화(initializer) 또는 프로퍼티 정의에서 기본 프로퍼티 값 할당으로 저장 프로퍼티에 대한 초기값을 설정할 수 있습니다. 이러한 동작은 다음 섹션(sections)에서 설명됩니다.

주의
저장 프로퍼티에 기본 값 할당하거나 초기화에서 초기 값을 설정할때, 프로퍼티 옵저버는 호출되지 않고,프로퍼티의 값이 직접 설정됩니다.

초기화(Initializers)

특정 타입의 새로운 인스턴스를 생성하기 위해 초기화(Initializers)가 호출됩니다. 이것은 매우 간단한 형식이며, 초기화는 매개변수 없는 인스턴스 메소드와 같으며, init키워드를 사용하여 작성합니다.

init() {
    // perform some initialization here
}

아래 예제는 화씨(Fahrenheit) 단위로 표시되는 온도를 저장하는 새로운 구조체 Fahrenheit를 정의합니다. Fahrenheit 구조체는 Double타입인, 저장 프로퍼티 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"

그 구조체는 저장된 온도를 32.0(화씨(Fehrenheit)온도계에서 물이 어는 시점)로 초기화하는, 매개변수 없는 단일 초기화 init을 정의합니다.

프로퍼티 기본 값(Default Property Values)

위에서 본것처럼, 초기화에서 저장 프로퍼티의 초기값을 설정할 수 있습니다. 아니면, 프로퍼티의 선언에서 프로퍼티 기본 값(default property value) 를 지정합니다. 프로퍼티가 정의될때 프로퍼티에 초기 값을 할당해서 프로퍼티 기본 값을 지정합니다.

주의
프로퍼티가 항상 동일한 초기 값을 가지는 경우, 초기화에서 값을 설정하는 것보다는 기본 값을 제공하는 것이 좋습니다. 결과는 같지만, 초기값을 주는 것은 프로퍼티의 초기화 선언에 더 가깝습니다. 더 짧고 명확한 초기화를 만들고 기본 값으로 부터 프로퍼티의 타입이 추론가능합니다. 또한, 기본 값은 기본 초기화와 초기화 상속을 쉽게 사용하도록 해주며, 이 챕터(chapter)의 뒷부분에 설명됩니다.

위의 Fahrenheit 구조체를 프로퍼티가 선언되는 시점에 temperature 프로퍼티에 대해 기본 값을 제공하는 더 간단한 형식으로 작성할수 있습니다.

struct Fahrenheit {
    var temperature = 32.0
}

초기화를 커스터마이징하기(Customizaing Initialization)

입력 매개변수와 옵셔널 프로퍼티 타입으로 초기화 과정을 커스터마이징(customize) 할 수 있거나 이전 섹션에서 설명했던것 처럼, 초기화 하는 동안 상수 프로퍼티를 할당할 수 있습니다.

매개변수 초기화(Initialization Parameters)

초기화를 커스터마이징(customize) 하는 과정에서 값의 타입과 이름을 정의하기 위해 , 초기화 정의에서 매개변수 초기화(initialization parameters)를 제공할 수 있습니다. 초기화 매개변수는 함수와 메소드 매개변수와 같은 기능과 문법을 가집니다.

다음 예제는 섭씨 단위로 표시된 온도를 저장하는 구조체 Celsius를 정의합니다. Celsius 구조체는 다른 온도계로부터 새로운 인스턴스를 초기화하는 2개의 사용자정의 초기화 init(ffromFahrenheit:)과 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 매개변수 이름으로 된 초기화 매개변수 하나를 가지고 있습니다. 두 초기화 모두 하나의 인자로 해당 섭씨(Ceisius) 값으로 변환하고 temperatureInCelsius 프로퍼티에 값을 저장합니다.

매개변수 이름과 인자 이름(Parameter Names and Argument Labels)

함수와 메소드 매개변수 처럼, 초기화 매개변수도 초기화 본문에서 사용하는 매개변수 이름과 초기화를 호출할때 사용하는 인자 라벨 모두 가질수 있습니다.

하지만, 초기화는 함수와 메소드와 같은 방식으로 괄호(parentheses ()) 앞에 식별가능한 함수 이름을 가지지 못합니다. 따라서, 초기화의 매개변수의 이름과 타입은 호출되는 초기화를 식별하는데 중요한 역할을 합니다. 이 때문에, Swift는 여러분이 초기화에서 제공하지 않으면, 모든(every) 매개변수에 대해 자동(automatic) 인자 라벨을 제공합니다.

다음 예제는 3개의 상수 프로퍼티 red, green, blue를 가진 구조체 Color를 정의합니다. 이러한 프로퍼티들은 빨강, 초록, 파랑 색상의 값을 나타내기 위해 0.0에서 1.0사이의 값을 저장합니다.

Color은 빨강, 초록, 파랑 요소들에 대해 Double 타입의 매개변수와 어울리는 3개의 이름을 초기화에서 제공합니다. Color는 3개의 색상 요소에 대해 동일한 값을 제공하기 위해 사용되는 하나의 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)

인자 라벨(argument labels)을 사용하지 않고서는 이러한 초기화를 호출하는 것이 불가능하다는 것을 주의합니다. 인자 라벨이 정의되어 있다면 초기화에서 반드시 사용되어야 하고, 생략하게 되면 컴파일 오류가 됩니다.

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

인자 라벨 없는 초기화 매개변수(Initializer Parameters Without Argument Labels)

초기화 매개변수에 대해 인자 라벨을 사용하는 것을 원치 않는 경우에는, 기본 동작을 오버라이드(override)하기 위한 매개변수에 대한 명시적인 인자 라벨 대신에 밑줄(underscore _)을 작성합니다.

다음은 위릐 초기화 매개변수(Initialization Parameters)의 Celsisus 예제의 확장된 버젼이며, 기존 섭씨 온도인 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

Celsius(37.0) 초기화 호출은 인자 라벨 없이도 의도가 명확합니다. 따라서 이름 없는 Double 값을 제공해서 호출되도록 할수 있도록 init(_ celsius: Double) 초기화를 작성하는 것이 적절합니다(appropriate).

옵셔널 프로퍼티 타입(Optional Property Types)

사용자정의 타입의 저장 프로퍼티가 논리적으로 값 없음(no-value)을 허용하는 경우, 프로퍼티를 옵셔널(optional) 타입으로 선언합니다. - 아마도 초기화하는동안 값을 설정할수 없거나, 나중에 값 없음(no value)을 허용해야 하기 때문일 것입니다. 옵셔널 타입의 프로퍼티는 자동으로 nil 값으로 초기화되며, 프로퍼티가 초기화하는 동안에 의도적으로 아직 값이 없음(no value yet)을 의미합니다.

다음 예제는 옵셔널 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."

설문 조사(survey question) 답변은 질문하기 전까지 알수가 없기에, response 프로퍼티는 String? 또는 "옵셔널 String"으로 선언됩니다. SurveyQuestion의 새로운 인스턴스가 초기화될때, 자동적으로 nil 기본 값이 할당되며, 아직 문자열 값 없음(no string yet)을 의미합니다.

초기화하는동안 상수 프로퍼티 할당하기(Assigning Constant Properties During Initialization)

초기화가 완료될때까지 명확한(definite) 값으로 설정하는 한, 초기화하는 어느 시점에 상수 프로퍼티에 값을 할당할 수 있습니다. 상수 프로퍼티에 값이 한번 할당되면, 나중에 수정할 수 없습니다.

주의
클래스 인스턴스에 대해, 클래스에서 상수 프로퍼티는 초기화 하는 동안에만 수정될 수 있습니다.

SurveyQuestion의 인스턴스가 한번 생성되면 질문(question)을 바꿀수 없다는 것을 가리키기 위해, 질문(question)의 text프로퍼티에 대해 변수 프로퍼티 보다는 상수 프로퍼티를 사용하여 위의 SurveyQuestion예제를 개선(revise)할 수 있습니다. 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 Initializer)을 제공합니다.. 기본 초기화(default initializer)는 모든 프로퍼티에 기본 값을 설정해서 새로운 인스턴스를 간단하게 생성합니다.

이 예제는 쇼핑 목록에서의 항목 이름, 수량, 구매상태를 캡슐화한 클래스 ShoppingListItem를 정의합니다.

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

ShoppingListItem클래스의 모든 프로퍼티는 기본 값을 가지고 있고, 상위클래스가 없는 기본 클래스 이기 때문에, ShoppingListItem은 자동적으로 모든 프로퍼티를 기본 값으로 설정해서 새로운 인스턴스를 생성하는 기본 초기화 구현을 가집니다(gains). (name 프로퍼티는 옵셔널 String이고, 코드에서 작성하지 않아도, 자동으로 기본 값 nil이 됩니다) 위 예제는 ShoppingListItem클래스에 대해 초기화 문법으로 클래스의 새로운 인스턴스를 생성하기 위해 기본 초기화를 사용하며, ShoppingListItem()으로 작성하고, item 변수에 새로운 인스턴스를 할당합니다.

구조체 타입에 대한 멤버단위 초기화(Memberwise Initializers for Structure Types)

구조체 타입은 사용자정의 초기화를 정의하지 않은 경우에, 자동적으로 멤버단위 초기화(memberwise initializer)를 합니다. 기본 초기화와 다르게, 구조체는 기본 값을 가지지 않는 저장 프로퍼티의 경우에, 멤버단위 초기화를 합니다.

멤버단위 초기화(memberwise initializer)은 새로운 구조체 인스턴스의 멤버 프로퍼티를 초기화하는 간단한 방법입니다. 새로운 인스턴스의 프로퍼티에 대한 초기 값은 이름을 기준으로 멤버단위 초기화로 전달될 수 있습니다.

아래 예제는 2개의 프로퍼티 width와 height를 가진 구조체 Size를 정의합니다. 두 프로퍼티 모두 기본 값이 0.0으로 할당되었기에 Double타입으로 추론됩니다.

Size 구조체는 자동으로 새로운 Size 인스턴스를 초기화하는데 사용할 수 있는 init(width:height:) 멤버단위 초기화를 합니다.

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

값 타입에 대한 초기화 위임(Initializer Delegation for Value Types)

초기화는 인스턴스의 초기화를 실행하기 위해, 다른 초기화를 호출할 수 있습니다. 이 과정을 초기화 위임(initializer delegation)이라고 하며, 초기화가 여러번 중복되는 것을 막아줍니다.

초기화 위임 하는 규칙과, 허용되는 위임의 형식은 값 타입(value types)과 클래스 타입(class types)에 따라 다릅니다. 값 타입(구조체와 열거형)은 상속을 지원하지 않고, 자체적으로 제공하는 다른 초기화로만 위임이 가능하기 때문에 , 초기화 위임 과정이 비교적 간단합니다. 하지만, 상속(Inheritance)에서 설명했던 것처럼, 클래스는 다른 클래스를 상속할수 있습니다. 이는 클래스는 초기화하는 동안에 상속받은 모든 저장 프로퍼티가 적절한 값이 할당되는 것을 보장해야 하는 추가적인 책임(responsibilities이 있다는 것을 의미합니다. 이러한 책임은 아래에 있는 클래스 상속과 초기화(Class Inheritance and Initialization)에 설명되어 있습니다.

값 타입에 대해, 사용자정의 초기화를 작성할때, 같은 값 타입의 다른 초기화를 참조하기 위해 self.init를 사용합니다. 초기화에서만 self.init를 호출할 수 있습니다.

값 타입에 대해 사용자정의 초기화를 정의하는 경우에, 타입에 대해 기본 초기화(구조체의 경우에 멤버단위 초기화)를 더 이상 사용할 수 없다는 것을 주의합니다. 이러한 제약은 더 복잡한 초기화에서 누군가 자동 초기화 중의 하나를 사용하다가 제공된 추가적인 필수 설정이 실수로 빠지는 상황을 막아줍니다.

주의
사용자정의 값 타입을 기본 초기화와 멤버단위 초기화로 초기화하길 원하고, 자신만의 사용자정의 초기화를 원하는 경우에, 값 타입의 원래 구현부 보다는 확장(extension)에서 사용자 정의 초기화를 작성합니다. 더 자세한 정보는 확장(Extensions)를 보세요.

다음 예제는 사각형 도형을 표현하기 위해 사용자정의된 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가지 방법중 하나로 초기화할 수 있습니다. - 기본적으로 origin과 size 프로퍼티 값이 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()는 기능적으로 구조체가 사용자정의 초기화를 가지지 않는 경우에 사용하는 기본 초기화와 같습니다. 이 초기화는 본문이 비어있으며, 비어 있는 중괄호로 표현됩니다 {}. 이 초기화를 호출하면 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:)는 기능적으로 구조체가 사용자정의 초기화를 가지지 않는 경우에 사용하는 멤버단위 초기화와 같습니다. 이 초기화는 단순히 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)

세번째 Rect 초기화 init(center:size:)는 약간(slightly) 더 복잡합니다. 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:) 초기화는 origin과 size의 새로운 값을 적절한 프로퍼티에 할당할 수 있습니다. 하지만, 정확히 그 기능을 제공하는 기존 초기화의 장점으로 init(center:size:) 초기화를 사용하는 것이 더 편리합니다.

주의
init()과 init(origin:size:) 초기화 정의하지 않고 이 예제를 작성하는 다른 방법은, 확장(Extensions)을 보세요.

클래스 상속화 초기화(Class Inheritance and Initialization)

클래스의 모든 저장 프로퍼티(상위클래스로 부터 상속받은 클래의 모든 프로퍼티를 포함)는 초기화하는 동안 초기 값이 반드시(must) 할당되어야 합니다.

Swift는 클래스 타입에 대해 모든 저장 프로퍼티가 초기 값을 가지도록 보장하는데 도움을 주는 2가지 종류의 초기화를 정의합니다. 이것들을 지정된 초기화(designated initializer)와 편리한 초기화(convenience initializers)라고 합니다.

지정된 초기화와 편리한 초기화(Designated Initializers and Convenience Initializers)

지정된 초기화(Designated initializers)는 클래스에서 우선되는(primary) 초기화 입니다. 지정된 초기화는 클래스에 있는 모든 프로퍼티들을 완전히 초기화 하고 상위클래스와 연결해서 초기화를 계속 진행하기 위해 해당 상위클래스 초기화를 호출합니다.

클래스는 지정된 초기화가 거의 없는 경향(tend)이 있고, 클래스에 하나만 가지고 있는 것이 일반적입니다. 지정된 초기화는 초기화하는 깔대기(funnel)지점이고, 상위클래스와 연결되어 초기화를 계속 진행합니다.

모든 클래스는 반드시 최소 하나의 지정된 초기화를 가져야 합니다. 일부의 경우에, 상위클래스로부터 상속받은 하나 이상의 지정된 초기화에 의해 이러한 요구사항이 만족되며, 아래에 있는 자동으로 초기화 상속하기(Automatic Initializer Inheritance)에서 설명됩니다.

편리한 초기화(Convenience initializer)는 보조적(secondary)이며, 클래스에 대한 초기화를 지원합니다. 지정된 초기화의 매개변수의 일부에 기본 값으로 설정되는 편리한 초기화처럼 같은 클래스에서 지정된 초기화를 호출하는 편리한 초기화를 정의할 수 있습니다. 특별한 케이스(specific use case)나 입력 값 타입에 대해 클래스의 인스턴스를 생성하기 위해 편리한 초기화를 정의할 수 있습니다.

클래스에서 필요하지 않는 경우에는 편리한 초기화를 제공할 필요가 없습니다. 일반적인 초기화 패턴이 시간을 절약하거나 클래스 초기화를 의도적으로 명확하게 만들때마다 편리한 초기화를 생성합니다.

지정된 초기화와 편리한 초기화 문법(Syntax for Designated and Convenience Initializers)

클래스에 대한 지정된 초기화(designated initializers)는 값 타입의 간단한 초기화와 같은 방법으로 작성합니다:

init(parameters) {
    statements
}

편리한 초기화(Convenience initializers)는 같은 스타일로 작성하지만, convenience 수직어를 ini키워드 앞에 붙이며, 공백으로 구분합니다:

convenience init(parameters) {
    statements
}

클래스 타입에 대한 초기화 위임(Initializer Delegation for Class Types)

지정된 초기화와 편리한 초기화 사이의 관계를 단순화 하기 위해, Swift는 초기화에서 위임을 호출하는 경우에 대해 3가지 규칙을 적용합니다.

규칙 1

지정된 초기화는 반드시 바로 위의 상위클래스의 지정된 초기화를 호출해야 합니다.

규칙 2

편리한 초기화는 반드시 같은(same) 클래스의 다른 초기화를 호출해야 합니다.

규칙 3

편리한 초기화는 궁극적(ultimately)으로 반드시 지정된 초기화를 호출해야 합니다.

기억하기 쉬운 방법:

  • 지정된 초기화는 항상 위쪽(up)으로 위임합니다. -> 상위클래스의 초기화 호출
  • 편리한 초기화는 항상 가로질러(across) 위임합니다. -> 같은 클래스의 다른 초기화 호출

이러한 규칙들은 아래그림에서 볼 수 있습니다.

여기에서, 상위클래스는 지정된 초기화 1개와 편리한 초기화 2개를 가지고 있습니다. 편리한 초기화 하나는 차례대로 지정된 초기화를 호출하는, 다른 편리한 초기화를 호출합니다. 이것은 위의 규칙 2와 3을 만족(satisfies)합니다. 상위클래스는 자체적으로 더이상(further)의 상위클래스가 없고, 규칙 1은 적용되지 않습니다.

이 그림에서 상위클래스는 지정된 초기화 2개와 편리한 초기화 1개를 가지고 있습니다. 편리한 초기화는 같은 클래스로부터 다른 초기화를 호출할 수 있기 때문에, 반드시 2개의 지정된 초기화중 하나를 호출해야 합니다. 이것은 위의 규칙 2와 3을 만족합니다. 위의 규칙 1을 만족하기 위해, 지정된 초기화 2개 모두 반드시 상위클래스의 지정된 초기화를 호출합니다.

주의
이러한 규칙들은 클래스 사용자가 각 클래스의 인스턴스를 생성(create)하는데 영향을 주지 않습니다. 위의 그림에서 모든 초기화는 자신이 속한 클래스의 완전히 초기화된 인스턴스를 생성하는데 사용될 수 있습니다.

아래 그림은 4개의 클래스에 대한 좀 더 복잡한 클래스 계층 구조를 보여줍니다.

이것은 클래스 초기화에 대한 깔대기(funnel) 처럼 이 계층 구조에서 지정된 초기화 동작 방법을 보여주며, 연결된 클래스들 간의 상호관계를 단순화 합니다.

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

Swift에서 클래스 초기화는 2단계 과정입니다. 첫번째 단계에서, 각 저장 프로퍼티는 클래스 도입부분에서 초기 값이 할당됩니다. 모든 저장 프로퍼티에 대해 초기 상태가 한번 결정되면, 2 단계가 시작되고, 각 클래스는 새로운 인스턴스가 사용할 준비가 되기전에 저장 프로퍼티를 사용자정의하기 위한 기회(opportunity)가 주어집니다.

2 단계(two0phase) 초기화 과정의 사용은 클래스 계층구조에서 각 클래스가 완전한 유연성을 제공하는동안에, 초기화를 안전하게 만듭니다. 2단계 초기화는 초기화되기 전에 프로퍼티에 접근하는 것을 막아주고, 프로퍼티 값이 다른 초기화에 의해서 예기치 않게 다른 값으로 설정되는 것을 막아줍니다.

주의
Swit의 2 단계 초기화 과정은 Objective-C에서의 초기화와 비슷합니다. 주요 차이점은 1 단계에서, Objective-C는 모든 프로퍼티를 0이나 null 값(0 또는 nil)을 할당합니다. Swift의 초기화 흐름은 사용자지정 초기값을 설정하는데 좀 더 유연하고, 0또는 nil이 유효한 기본값이 아닌 타입을 처리할 수 있습니다.

Swift의 컴파일러는 2 단계 초기화가 오류없이 완료되도록 4개의 안전 검사(safety-checks) 수행합니다.

안전 검사 1

지정된 초기화는 반드시 상위 클래스 초기화로 위임하기 전에 클래스에 의해 도입된 모든 프로퍼티가 초기화 되는 것을 보장합니다.

위에 언급된, 객체에 대한 메모리는 모든 저장 프로퍼티가 초기 상태가 되면 완전히 초기화된 것으로 간주합니다. 이 규칙을 만족시키려면(satisfied)), 지정된 초기화는 위로 전달되기 전에 반드시 자신의 모든 프로퍼티가 초기화 되도록 해야 합니다.

안전 검사 2

지정된 초기화는 반드시 상속된 프로퍼티에 값을 할당하기 전에 상위클래스 초기화로 위임해야 합니다. 그렇지 않으면, 지정된 초기화가 할당한 새 값은 자신의 초기화에서 상위클래스에 의해 덮어 쓰여질것입니다.

안전 검사 3

편리한 초기화는 반드시 모든(any) 프로퍼티(같은 클래스에 정의된 프로퍼티 포함)에 값을 할당하기 전에 다른 초기화로 위임해야 합니다. 그렇지 않으면, 편리한 초기화로 할당한 새 값은 자신의 클래스의 지정된 초기화에 의해 덮어 쓰여질것입니다.

안전 검사 4

초기화는 모든 인스턴스 메소드를 호출할 수 없으며, 모든 인스턴스 프로퍼티의 값 또는, 1 단계 초기화가 완료될때까지 self와 관련된 값을 읽을수 없습니다.

클래스 인스턴스는 1 단계가 끝날때까지 완전히 유효하지 않습니다. 일단 클래스 인스턴스가 1 단계 끝에서 유효하는 것을 알고 있으면, 프로퍼티만 사용할 수 있고, 메소드만 호출할 수 있습니다.

다음은 위의 4개의 안전검사를 기반으로, 2 단계 초기화하는 방법입니다.

1 단계

  • 지정된 초기화나 편리한 초기화는 클래스에서 호출됩니다.
  • 클래스의 새로운 인스턴스에 대한 메모리가 할당됩니다. 그 메모리는 아직 초기화되지 않았습니다.
  • 클래스에 대한 지정된 초기화는 클래스가 가진 값으로 도입된 모든 저장 프로퍼티를 확인합니다. 이러한 저장 프로퍼티에 대한 메모리는 이제 초기화 됩니다.
  • 지정된 초기화는 자신의 저장 프로퍼티에 대해 동일한 동작을 하기 위해, 상위클래스 초기화로 넘겨줍니다(hands off).
  • 체인(chain)의 최상단에 도착할때까지 클래스 상속 체인은 계속 위로 올라갑니다.
  • 일단 체인(chain)의 최상단에 도착하고, 체인의 마지막 클래스는 모든 저장 프로퍼티가 값을 가지고 잇는 것을 보장하며, 그 인스턴스의 메모리는 완전히 초기화된 것으로 간주되고(considered), 1 단계가 완료됩니다.

2 단계

  • 체인(chain)의 최상단에서 아래로 내려오며, 체인에서 각 지정된 초기화는 인스턴스를 추가적으로 사용자정의 할수 있는 옵션이 있습니다. 초기화는 이제 self사용이 가능하고 프로퍼티를 수정하며, 인스턴스 메소드를 호출할 수 있습니다.
  • 마지막으로, 체인(chain)에서 편리한 초기화는 인스턴스를 사용자정의 하고 self로 작업할 수 있습니다.

다음은 가상의 하위클래스와 상위클래스에 대한 1 단계 초기화를 호출하는 방법을 보여줍니다.

이 예제에서, 초기화는 하위클래스에서 편리한 초기화를 호출하여 시작합니다. 편리한 초기화는 아직 모든 프로퍼티를 수정할 수 없습니다. 같은 클래스이 지정된 초기화로 위임합니다.

지정된 초기화는 안전 검사 1에 따라, 하위클래스의 모든 프로퍼티가 값을 가지고 있는지 확인합니다. 그리고나서 체인(chain)의 초기화를 계속하기 위해, 상위클래스에 있는 지정된 초기화를 호출합니다.

상위클래스의 지정된 초기화는 상위클래스의 모든 프로퍼티가 값을 가지고 있는지 확인합니다. 초기화할 상위클래스가 더이상 없고, 더 이상 위임할 필요도 없습니다.

상위클래스의 모든 프로퍼티가 초기값을 가지자 마자, 메모리는 완전히 초기화된것으로 간주되고, 1 단계는 완료됩니다.

다음은 2단계에서 같은 초기화 호출하는 방법을 보여줍니다.

상위클래스의 지정된 초기화는 이제 인스턴스를 추가로 커스터마이징(customize)하는 기회(opportunity)가 있습니다. (비록 그렇게 할 필요가 없습니다)

일단 상위클래스의 지정된 초기화는 완료되면, 상위클래스의 지정된 초기화는 추가적으로 커스터마이징(customization)을 수행할 수 있습니다. (다시 말하지만, 그렇게 할 필요가 없습니다.)

마지막으로, 일단 하위클래스의 지정된 초기화가 완료되면, 원래 호출된 편리한 초기화는 추가적인 커스터마이징(customization)을 수행할 수 있습니다.

초기화 상속과 오러라이딩(Initializer Inheritance and Overriding)

Objective-C 에서의 하위클래스와 다르게, Swift 하위클래스는 기본적으로 상위클래스 초기화를 상속받지 않습니다. Swift의 접근법은 상위클래스의 간단한 초기화가 좀더 특별한 하위클래스에 의해 상속되고 완전하지 않거나 제대로 초기화되지 않은 하위클래스의 새로운 인스턴스를 생성하기 위해 사용되는 상황을 막아줍니다.

주의
상위클래스 초기화는 특정 상황(certain circumstances)에서 상속되지만, 그렇게 하는 것이 안전하고 적절 할때만 가능합니다. 더 자세한 정보는, 아래의 자동 초기화 상속(Automatic Initializer Inheritance)를 보세요.

상위클래스 처럼, 하나 이상의 동일한 초기화를 제공하기 위해 하위클래스를 사용자정의하길 원하는 경우에, 하위클래스에서 이러한 초기화의 사용자정의 구현을 제공할 수 있습니다.

상위클래스 지정된(designed) 초기화와 일치하는 하위클래스 초기화를 작성할때 , 지정된 초기화의 오버라이딩(override)을 효과적(effectively)으로 제공하고 있습니다. 따라서, 상위클래스의 초기화 정의 전에 override 수정자를 작성해야합니다. 이것은 자동으로 제공되는 기본 초기화를 오버라이팅하는 경우에도 마찬가지이며, 기본 초기화(Default Initializers)에서 설명되어 있습니다.

오버라이드 된(overridden) 프로퍼티, 메소드 또는 서브스크립트와 같이, override 수식어의 존재는 
오버라이드 되도록 상위클래스가 일치하는 지정된 초기화를 가지고 있는지 확인하기 위해 Swift에게 알려주고, 오버라이딩하는 초기화에 대한 매개변수가 의도한데로 지정되었는지 확인합니다.

주의
상위클래스 지정된 초기화를 오버라이딩할때 항상 override 수식어를 작성하며, 상위클래스의 초기화 구현이 편리한 초기화인 경우에도 마찬가지 입니다.

반대로(conversely), 하위클래스 초기화를 상위클래스 편리한(convenience) 초기화와 일치하는 경우에, 상위클래스 편리한 초기화는 결코 하위클래스에서 직접 호출할 수 없으며,위의 클래스 타입에 대한 초기화 위임(Initializer Delegation for Class Types)에 설명된 규칙을 따릅니다. 따라서, (엄밀히 말하면) 하위클래스는 상위클래스의 초기화를 제공하지 않습니다. 결과적으로, 상위클래스 편리한 초기화와 일치하는 구현을 제공할때 override 수식어를 작성하지 않습니다.

아래 예제는 기본 클래스 Vehicle를 정의합니다. 이 기본 클래스는 저장 프로퍼티 numberOfWheels를 기본 Int 값 0으로 선언합니다. numberOfWheels 프로퍼티는 탈것의 특징의 String 설명을 생성하기 위해 계산 프로퍼티 description에 의해 사용됩니다.

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

Vehicle 클래스는 저장 프로퍼티에 대해서만 기본 값을 제공하고, 어떠한 사용자정의 초기화를 제공하지 않습니다. 결과적으로, 자동으로 기본 초기화를 받으며, 기본 초기화(Default Initializers)에서 설명되어 있습니다. 클래스에 대한 기본 초기화(가능한경우에)는 항상 지정된 초기화이고, 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의 상위클래스의 지정된 초기화와 일치하고, 이 초기화의 Bicycle버젼은 override 수식어로 표시됩니다.

Bicycle에 대한 init() 초기화는 Bicycle 클래스의 상위클래스 Vehicle에 대한 기본 초기화를 호출하는 super.init() 호출로 시작합니다. 이렇게 하면 상속된 프로퍼티 numberOfWheels 는 Bicycle가 프로퍼티를 수정할 기회를 가지기 전에, Vehicle에 의해 초기화됩니다. super.init() 호출한 후에, numberOfWheels의 원래 값은 새로운 값 2로 교체됩니다.

Bicycle의 인스턴스를 생성하는 경우에, numberOfWheel 프로퍼티가 업데이트되었는지 보기 위해, 상속받은 계산 프로퍼티 description를 호출할 수 있습니다.

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

주의
하위클래스는 초기화하는 동안 상속받은 변수 프로퍼티를 수정할 수 있지만, 상속받은 상수 프로퍼티는 수정할 수 없습니다.

자동 초기화 상속(Automatic Initializer Inheritance)

위에서 언급한것 처럼, 하위클래스는 기본적으로 상위클래스의 초기화를 상속하지 않습니다. 하지만, 상위클래스 초기화는 특정 조건을 충족하면, 자동으로 상속됩니다. 실제로, 이것은 대부분의 일반적인 상황에서 초기화를 작성할 필요가 없다는 의미이고, 자동으로 상속하는 것이 안전할때는 적은 노력으로 상위클래스 초기화를 상속할수 있습니다.

하위클래스에서 모든 새로운 프로퍼티에 대해 기본값을 제공한다고 가정한 경우에, 다음 2가지 규칙이 적용됩니다.

규칙 1

하위클래스가 어떠한 지정된 초기화도 정의하지 않은 경우, 자동으로 상위클래스의 모든 지정된 초기화가 상속됩니다.

규칙 2

하위클래스가 상위클래스의 모든(all) 지정된 초기화(규칙 1 처럼 상속받거나 정의하면서 사용자정의 구현을 제공) 구현을 제공하는 경우, 자동으로 상위 클래스의 모든 편리한 초기화가 상속됩니다

이러한 규칙들은 하위클래스에 더 많은 편리한 초기화를 추가하는 경우에도 적용합니다.

주의
규칙 2를 만족하는 하위클래스는 하위클래스의 편리한 초기화처럼, 상위클래스의 지정된 초기화를 구현할 수 있습니다.

지정된 초기화와 편리한 초기화 동작(Designated and Convenience Initializers in Action)

다음 예제는 지정된 초기화, 편리한 초기화, 자동으로 초기화 상속의 동작을 보여줍니다. 이 예제는 3개의 클래스 Food, RecipeIngredient, ShoppingListItem 계층구조를 정의하고 초기화가 상호작용하는 것을 보여줍니다.

계층구조에서 기본 클래스는 Food 이며, 식품(foodstuff)의 이름을 캡슐화하는 간단한 클래스입니다. Food 클래스 인스턴스는 하나의 String프로퍼티 name와 Food 인스턴스를 생성하는 2개의 초기화를 제공합니다.

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

아래 그림은 Food 클래스에 대한 초기화 체인(chain)을 보여줍니다.

클래스들은 기본 멤버단위(memberwise) 초기화를 하지 않고, Food클래스는 하나의 인자 name를 가지는 지정된 초기화 하나를 제공합니다. 이 초기화는 특정 이름으로 새로운 Food 인스턴스를 생성하는데 사용될 수 있습니다.

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

Food 클래스에서 init(name: String) 초기화는 새로운 Food인스턴스의 모든 저장 프로퍼티가 완전히 초기화된 것을 보장하기 때문에, 지정된(designated) 초기화로 제공됩니다. Food 클래스는 상위클래스를 가지고 있지 않고, init(name: String) 초기화는 초기화를 완료하기 위해, super.init() 호출이 필요하지 않습니다.

또한, Food 클래스는 인자가 없는 편리한(convenience) 초기화 init()을 제공합니다. init() 초기화는 Food 클래스의 init(name: String)에 위임하여 [Unnamed]의 이름으로 새로운 음식에 대한 기본 이름을 제공합니다.

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

계층구조에서 두번째 클래스는 Food의 하위클래스인 RecipeIngredient 입니다. RecipeIngredient 클래스는 요리 레시피 재료를 모델링하였습니다. Int 프로퍼티 quantity(Food로 부터 상속받은 name프로퍼티 이외에)와 RecipeIngredient 인스턴스를 생성하는 2개의 초기화를 정의합니다.

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 클래스는 새로운 RecipeIngredient 인스턴스의 모든 프로퍼티를 채우기 위해 사용할 수 있는 지정된 초기화 init(name: String, quantity: Int) 1개를 가집니다. 이 초기화는 전달된 quantity 인자를 RecipeIngredient에 의해 도입된 새로운 프로퍼티인 quantity 프로퍼티에 할당하면서 시작합니다. 그런 후에, 그 초기화는 Food 클래스의 init(name: String) 초기화를 위로 위임합니다. 이 과정은 위의 2 단계 초기화(Two-Phase Initialization)의 안전검사 1을 만족합니다.

RecipeIngredient는 이름 하나만으로 RecipeIngredient인스턴스를 생성하는데 사용되는 편리한 초기화 init(name: String)을 정의합니다. 이 편리한 초기화는 명시적인 수량 없이 생성된 모든 RecipeIngredient 인스턴스에 대해 quientity을 1로 가정합니다(assumes). 이 편리한 초기화 정의는 RecipeIngredient 인스턴스를 보다 빠르고 편리하게 만들수 있고, 한개의 수량(quantity)을 가진 RecipeIngredient인스턴스 여러개를 생성할때 중복된 코드를 피하게 해줍니다. 이 편리한 초기화는 클래스의 지정된 초기화에 단순히 위임하며, quantity 값 1을 전달합니다.

편리한 초기화 init(name: String)은 Food의 지정된(designated) 초기화 init(name: String)와 같은 매개변수를 가지는 RecipeIngredient에 의해 제공됩니다. 이 편리한 초기화는 상위클래스의 지정된 초기화를 오버라이드하기 때문이며, override 수식어로 표시되어야 합니다. (초기화 상속과 오버라이딩(Initializer Inheritance and Overriding)에서 설명되어 있습니다)

RecipeIngredient는 편리한 초기화 처럼 init(name: String) 초기화를 제공하며, 그럼에도 불구하고 RecipeIngredient는 상위클래스의 모든 지정된 초기화의 구현을 제공합니다. 따라서, RecipeIngredient는 상위클래스의 모든 편리한 초기화를 자동으로 상속받습니다.

이 예제에서, RecipeIngredient에 대한 상위클래스는 하나의 편리한 초기화 init()를 가지고 있는 Food입니다. 이 초기화는 RecipeIngredient에 의해서 상속됩니다. init() 함수의 상속된 버젼은 Food 버젼과 똑같은 방식이며, Food 벼젼보다 init(name: String)의 RecipeIngredient 벼젼에 위임하는 것이 다릅니다(except).

이러한 3가지 초기화 모두 새로운 RecipeIngredient 인스턴스를 생성하기 위해 사용됩니다.

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

계층구조에서 3번째와 마지막 클래스는 RecipeIngredient의 하위클래스인 ShoppingListItem입니다. ShoppingListItem 클래스는 쇼핑 목록에서 조리법 재료에 대해 모델링한 한것입니다.

쇼핑 목록에서 모든 항목은 미구입(unpurchased)으로 시작합니다. 이 사실을 표현하기 위해, ShoppingListItem에는 Boolean 프로퍼티 purchased를 도입하였으며, 기본 값은 false 입니다. ShoppingListItem은 또한 ShoppingListItem인스턴스의 텍스트 설명을 제공하는 계산 프로퍼티 description를 추가하였습니다.

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

주의
ShoppingListItem은 쇼핑 목록에 있는 항목은 항상 미구입(unpurchased)으로 시작하기 때문에, purchased에 대한 초기값을 제공하는 초기화를 정의하지 않습니다.

아래 그림은 3개 클래스 전체 초기화 체인(chain)을 보여줍니다.

새로운 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 ✘

여기에서, 새로운 배열 breakfastList는 3개의 새로운 ShoppingListItem 인스턴스를 포함하는 배열 리터럴로 생성됩니다. 배열의 타입은 [ShoppingListItem]으로 추론됩니다. 배열이 생성된 후에, 배열의 시작부분에 있는 ShoppingListItem의 이름은 "[Unnamed]"에서 "Orange juice"로 변경되고 구매된것으로 표시됩니다. 배열에서 각 항목의 설명을 출력하여 기본 상태가 예상대로 설정되어 있음을 보여줍니다.

실패할 수 있는 초기화(Failable Initializers)

클래스, 구조체, 열거형의 초기화가 실패할 수 있다는 것에 대해 정의하는 것이 유용할 때가 있습니다. 이러한 실패는 유효하지 않은 초기화 매개변수 값, 필요한 외부리소스가 없거나, 초기화를 성공하지 못하게 하는 다른 조건들에 의해서 발생할 수 있습니다.

실패할 수 있는(failable) 초기화 조건을 처리하려면, 클래스, 구조체, 열거형을 정의하면서 하나 이상의 실패할 수 있는 초기화를 정의해야 합니다. init키워드 뒤에 물음표를 붙여서(init?) 실패할수 있는 초기화를 작성합니다.

주의
같은 매개변수 타입과 이름으로 실패할수 있고 실패하지 않는 초기화를 정의할 수는 없습니다.

실패할 수 있는 초기화는 타입의 옵셔널(optional) 값으로 초기화하여 생성합니다. 초기화 실패가 발생할 수 있는 지점을 가리키기 위해 실패할 수 있는 초기화를 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"

아래 예제는 상수 String프로퍼티 species를 가진 구조체 Animal을 정의합니다. 또한, Animal 구조체는 1개의 매개변수 species을 가진 실패할수 있는 초기화를 정의합니다. 이 초기화는 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"

실패할수 있는 초기화의 species매개변수에 빈 문자열이 전달되는 경우, 그 초기화(initializer)는 초기화 실패가 됩니다.

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"

주의
빈 문자열 값("Giraffe"보다는 "")을 확인하는 것은 옵셔널(optional) String 값이 없는지 알리기 위해 nil에 대해서 확인하는 것과는 다릅니다. 위의 예제에서, 빈 문자열("")은 유효하며, 옵셔널이 아닌 String입니다. 하지만, 동물에 대해서는 species프로퍼티의 값이 빈 문자열인 것은 어울리지 않습니다. 이러한 제한사항(restriction)을 모델링 하려면, 실패할수 있는 초기화는 빈 문자열인 경우에, 초기화는 실패하게 됩니다.

열거형에 대한 실패할 수 있는 초기화(Failable Initializer for Enumerations)

하나 이상의 매개변수를 기반으로 적절한 열거형 케이스(case)를 선택하기 위해 실패할 수 있는 초기화를 사용할 수 있습니다. 제공된 매개변수가 열거형 케이스(case)와 일치하지 않는 경우에, 초기화는 실패할 수 있습니다.

아래 예제는 3개의 가능한 상태(kelvin, celsius, fahrenheit)로 열거형 TemperatureUnit를 정의합니다. 실패할 수 있는 초기화는 Character값의 온도 기호를 표현하기 위해 적절한 열거형 케이스(case)를 찾기 위해 사용됩니다.

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

3개의 가능한 상태에 대해 적절한 열거형 케이스를 고르기 위해, 실패할 수 있는 초기화를 사용할 수 있고 매개변수가 이러한 상태들 중 하나와 일치하지 않으면 초기화는 실패하게 됩니다.

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

원시 값을 사용하는 열거형에 대한 실패할 수 있는 초기화(Failable Initializers for Enumerations with Raw Values)

원시 값(raw value)을 사용하는 열거형은 자동으로 실패할 수 있는 초기화 init?(rawValue:)를 받으며, 적절한 원시 값 타입의 매개변수 rawValue를 가지고 열거형 케이스 중 하나를 찾아 선택하거나 일치하는 값이 없는 경우 초기화는 실패하게 됩니다.

위의 TemperatureUnit 예제를 Character 타입의 원시값을 사용하여 다시 작성 할수 있고 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 is not a defined temperature unit, so initialization failed.")
}
// Prints "This is not a defined temperature unit, so initialization failed."

초기화 실패의 전달(Propagation of Initialization Failure)

클래스, 구조체, 열거형의 실패할수 있는 초기화는 같은 클래스, 구조체, 열거형의 다른 실패할 수 있는 초기화에 위임할수 있습니다. 비슷하게, 하위 클래스의 실패할 수 있는 초기화는 상위클래스 실패할 수 있는 초기화까지 위임할 수 있습니다.

어떤 경우든지, 다른 초기화로 위임하여 실패하게 되는 경우, 전체 초기화 과정은 즉시 실패하고, 더 이상의 초기화 코드는 실행되지 않습니다.

주의
실패할 수 있는 초기화는 실패하지않는 초기화에 위임할 수 있습니다. 기존 초기화 과정에 실패할 가능성(potential)이 있는 상태를 추가해야 하는 경우에 이 방법을 사용하며, 그렇지 않으면 실패하지 않습니다.

아래 예제는 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)
    }
}

CartItem에 대한 실패할 수 있는 초기화는 quantity의 값이 1이상을 가지는지 확인하는 것으로 시작합니다. quantity가 유효하지 않는 경우, 전체 초기화 과정은 즉시 실패하고 더 이상 초기화 코드는 실행되지 않습니다. 게다가(likewise), Product에 대한 실패할 수 있는 초기화는 name 값을 검사하고, name이 빈 문자열인 경우, 초기화 과정은 즉시 실패합니다.

비어있지않는 이름과 1개 이상의 개수(quantity)로 CartItem인스턴스를 생성하는 경우, 초기화는 성공합니다.

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"

실패할 수 있는 초기화 오버라이딩하기(Overriding a Failable Initializer)

다른 초기화와 마찬가지로, 하위클래스에서 상위클래스의 실패할 수 있는 초기화를 오버라이딩할수 있습니다. 대신에, 하위클래스의 실패하지않는(nonfailable) 초기화로 상위클래스의 실패할 수 있는 초기화를 오버라이딩 할수 있습니다. 이렇게 하면 상위클래스의 초기화가 실패하더라도 초기화는 실패할수 없는 하위클래스를 정의하는 것이 가능합니다.

상위클래스의 실패할 수 있는 초기화를 실패하지 않는 하위클래스 초기화로 오버라이딩 하는 경우에, 상위클래스 초기화에 위임하는 유일한 방법은 상위클래스의 실패할수 있는 초기화의 결과를 강제 언래핑(force-unwrap) 하는 것입니다.

주의
실패하지 않는 초기화로 실패할수 있는 초기화를 오버라이딩 할수 있지만 다른 방법은 없습니다.

아래 예제는 클래스 Document를 정의합니다. 비어있지 않는 문자열 값 또는 nil이지만, 빈 문자열이 될수는 없는 name 프로퍼티로 초기화 할수 있는 문서(document)를 모델링한 것입니다.

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의 하위클래스 AutomaticallyNameDocument를 정의합니다. AutomaticallyNamedDocument 하위클래스는 Document에 의해 도입된 지정된 초기화 모두를 오버라이딩 합니다. 이러한 오버라이딩은 이름 없이 인스턴스가 초기화 되거나 init(name:) 초기화로 빈 문자열이 전달되는 경우에, AutomaticallyNamedDocument 인스턴스가 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은 실패하지 않는 init(name:) 초기화로 상위클래스의 실패할 수 있는 init?(name:) 초기화를 오버라이딩합니다. AutomaticallyNamedDocument는 상위클래스와 다른 방법으로 빈 문자열을 처리하기 때문에, 그 초기화는 실패할 필요가 없고, 실패하지 않는 버젼의 초기화를 대신 제공합니다.

하위클래스의 실패하지 않는 초기화의 구현에서 상위클래스의 실패할수 있는 초기화를 호출해서 초기화에서 강제 언래핑을 사용할수 있습니다. 예를들어, 아래의 UntitledDocument 하위클래스는 항상 "[Untitled]"이름을 가지고, 상위클래스를 초기화 하는 동안에 실패할 수 있는 init(name:) 초기화를 사용합니다.

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

이 경우에, 상위클래스의 init(name:) 초기화가 이름이 빈 문자열로 호출되었으며, 강제 언래핑 동작으로 런타임 오류가 발생합니다. 하지만, 문자열 상수로 호출되었기 때문에, 초기화가 실해하지 않는 것을 볼수 있으며, 이 경우에 런타임 오류가 발생 할 수 없습니다.

실패할 수 있는 초기화 init!(The init! Failable Initializer)

일반적으로 init 키워드 뒤에 물음표를 붙여서(init?) 적절한 타입의 옵셔널 인스턴스를 생성하는 실패할 수 있는 초기화를 정의합니다. 또는, 적절한 타입의 암시적으로 언래핑된 옵셔널 인스턴스를 생성하는 실패할 수 있는 초기화를 정의 할 수 있습니다. 이것을 init키워드 뒤에 물음표 대신 느낌표를 붙이는 것으로 수행합니다. (init!)

init?을 init!으로 위임할 수 있고 그반대도 가능하고, init?을 init!로 오버라이딩 할수 있고 반대도 가능합니다. 또한, init을 inti!로 위임할수 있으며, 그렇게 하면 init! 초기화가 실패하는 경우에 assertion이 발생할 것입니다.

필수 초기화(Required Initializers)

클래스의 모든 하위클래스가 반드시 초기화를 구현해야 하는 것을 가리키기 위해, 클래스 초기화 정의 앞에required 수식어를 작성합니다.

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

필수적인 초기화가 체인(chain)에서 하위클래스에 적용되는 것을 가리키기 위해, 항상 하위클래스의 필수 초기화의 구현 앞에 required 수정자를 작성해야 합니다. 필수 지정된 초기화를 오버라이딩 할때는 override 수식어를 작성하지 않습니다.

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

주의
상속된 초기화 요건을 충족할수 있는 경우, 필수 초기화의 명시적인 구현을 제공할 필요가 없습니다.

클로져나 함수로 기본 프로퍼티 값 설정하기(Setting a Default Property Value with a Closure or Function)

저장 프로퍼티의 기본 값이 약간의 사용자정의 또는 설정이 필요한 경우에, 프로퍼티에 대한 기본 값을 커스터마이징(customized)을 할수 있는 클로져나 전역 함수를 사용할 수 있습니다. 타입의 새로운 인스턴스의 프로퍼티가 초기화될때마다, 클로져와 함수는 호출되고, 프로퍼티의 기본값으로 할당된 값을 반환 합니다.

이러한 클로져나 함수의 종류는 일반적으로 프로퍼티와 같은 타입의 임시 값을 생성하며, 원하는 초기 상태를 나타내는 값으로 조정하고, 프로퍼티의 기본 값으로 사용되도록 임시 값을 반환합니다.

다음은 클로져가 기본 프로퍼티 값을 제공하는데 사용할수 있는 방법에 대한 핵심 개요(skeleton outline)입니다.

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 1개를 가지고 있습니다. 배열에서 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]
    }
}

새로운 Chessboard 인스턴스가 생성될때마다, 그 클로져는 실행되고, boardColors의 기본값은 계산되고 반환됩니다. 위 예제에서 클로져는 계산하고 설정합니다. 임시 배열 temporaryBoard에 보드에 있는 각 사각형에 대해 적절한 색상을 계산하고 설정하고, 일단 설정이 완료되면 클로져의 반환 값으로 임시 배열을 반환합니다. 반환된 배열 값은 boardColors에 저장되고 squareIsBlackAt(row:column:) 유틸리티 함수로 조회할 수 있습니다.

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


'Swift > Language Guide' 카테고리의 다른 글

Optional Chaining  (0) 2018.09.18
Deinitialization  (0) 2018.09.18
Initialization  (0) 2018.09.17
Inheritance  (0) 2018.08.30
Subscripts  (0) 2018.08.30
Methods  (0) 2018.08.30
Posted by 까칠코더