GGURUPiOS

Swift 공식 문서 정리 - (14) Initialization 본문

Swift/공식문서 정리 ( 문법 )

Swift 공식 문서 정리 - (14) Initialization

꾸럽 2023. 4. 28. 15:43

번역본과 영문판 공식문서(공식 사이트)를 번역하면서 보기 때문에 용어가 같은 의미인데 다르게 쓰이는게 많네요.

예를 들면

이니셜라이저 = 초기자, 생성자 (init)

인자 = 인수 (argument)

번역본에서도 initializer 를 그때그때 초기자, 이니셜라이저로 번갈아 부르더군요..

참고 바랍니당

초기화 부분이 좀 양이 많네요 후.. 화이팅

Initialization ( 초기화 )

초기화는 사용할 클래스, 구조체, 열거형의 인스턴스를 준비하는 과정임

이 과정에는 해당 인스턴스의 각 저장된 프로퍼티에 대한 초기 값을 설정하고

새 인스턴스를 사용할 준비가 되기 전에 필요한 다른 설정 또는 초기화를 수행하는 작업이 포함됨

특정 타입의 새 인스턴스를 생성하기 위해 호출할 수 있는 특수 메서드와 같은 초기화 프로그램을 정의하여 이 초기화 프로세스를 구현함

클래스 타입의 인스턴스는 해당 클래스의 인스턴스가 할당 해제되기 직전에 사용자 지정 정리를 수행하는 deinitializer 를 구현할 수도 있음

저장 프로퍼티의 초기 값 설정

클래스 및 구조체는 모든 저장 프로퍼티를 적절한 초기 값으로 설정해야 함

저장 프로퍼티는 불확실한 상태가 될 수 없음.

이니셜라이저(생성자) 내에서 또는 프로퍼티 정의의 일부로 기본 값을 할 당 하여 저장 프로퍼티의 초기값 설정 가능

기본값을 할당하거나, 생성자 내에서 초기값을 설정하면 프로퍼티 옵저버를 호출하지 않고 해당 프로퍼티의 값이 직접 설정 됨.

이니셜 라이저

이니셜라이저는 특정 타입의 새 인스턴스를 생성하기 위해 호출됨

가장 간단한 형태는 init 키워드를 사용하여 파라미터가 없는 인스턴스 메서드와 같음

init() {
	// 초기화 수행 코드
}
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"

// temparature의 기본값을 init 생성자 안에서 설정해 준 모습

기본 프로퍼티

프로퍼티의 선언과 동시에 값을 할당하면 초기값으로 사용 가능

항상 같은 초기 값을 갖는다면 기본 프로퍼티를 사용하는 것이 좋음

프로퍼티에 타입을 선언하지 않아도 컴파일러는 초기 값을 참조해서 타입을 추론함

이 기본값은 상속시에 함께 상속 됨

struct Fahrenheit {
    var temperature = 32.0
}

// 프로퍼티 선언과 동시에 초기 값 할당 

커스터마이징 초기화

초기화 프로세스를 입력 값과 옵셔널 프로퍼티 타입 혹은 상수 값을 할당해서 커스터 마이징 가능

초기화 파라미터

초기화 정의에 파라미터를 정의해서 사용할 수 있음

init(parameter: Double) {

// 선언된 변수 및 상수 초기화 과정

} 이런식으로 초기화 할 수 있다는 말임

물론 여러 생성자를 생성해서 사용 가능함

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

파라미터 이름 및 인자 레이블

메소드 파라미터와 초기화 파라미터 모두 파라미터 이름과 인자 레이블을 갖지만

생성자는 특정 메소드에서 지정하는 메소드 이름을 지정하지 않고 이니셜라이저 식별자로 파라미터를 사용 함

→ 이니셜라이저에는 따로 함수 이름이 없기 때문에 파라미터의 이름과 타입은 어떤 이니셜라이저를 호출해야 하는지 식별하는 데 중요한 역할을 함.

모든 파라미터는 인자 레이블을 갖는데 만약 사용자가 레이블을 지정하지 않으면 Swift가 자동으로 하나를 할당 해 제공 함

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

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 ( 값 없음 ) 을 가질 수 있는 저장 프로퍼티가 있는 경우 옵셔널로 선언해서 사용할 수 있음. 옵셔널 프로퍼티는 자동으로 nil 로 초기화 됨

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

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

이니셜라이저에서는 상수 프로퍼티에 값을 할당하는 것도 가능 함

초기화 중 언제든지 상수 프로퍼티에 값을 할당할 수 있음.

값이 할당되면 더 이상 수정 불가. ( let, var의 차이를 알면 당연한 말인듯..? )

클래스 인스턴스에서 상수 프로퍼티는 초기화 중 그 클래스 안에서만 변경이 가능

서브클래스에서는 변경이 불가능 함

프로퍼티를 let으로 선언해서 이 프로퍼티는 처음에 초기화 되면 변경되지 않는 프로퍼티 라는 것을 표현 가능

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

기본 이니셜라이저

만약 모든 프로퍼티의 초기값이 설정되어 있고, 하나의 초기자도 정의하지 않았다면

스위프트는 모든 프로퍼티를 기본 값으로 초기화 하는 기본 초기자를 제공해 줌

→ 즉 모든 프로퍼티가 빠짐없이 기본값이 있으면 굳이 Init() 을 정의 안해도

모든 프로퍼티의 기본 값으로 초기화 하는 init() 을 자동으로 제공해준다는 말인듯함

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

// 위의 클래스에서는 init() 을 정의 하지 않았지만 기본값이 있기 때문에 init() 정의를 안해도 생성 가능

구조체 타입을 위한 멤버별?(멤버쪽) 이니셜라이저

구조체 타입은 초기화를 정의하지 않는 경우 자동으로 멤버별 초기화를 받음.

기본 이니셜라이저 와 다르게 멤버쪽 이니셜라이저는 프로퍼티가 기본 값이 없어도

커스텀 이니셜라이저를 정의하지 않았다면 멤버쪽 이니셜라이저를 제공해 줌

이 초기자는 선언한 모든 프로퍼티를 인자로 사용 함

→ 간단히 말해서 구조체에 선언된 프로퍼티들을 초기화 하도록 모든 인자를 받는다는 말.

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

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

// 하지만 구조체 자체에 프로퍼티가 기본값이 있으면, 생략 가능 하다. 

값 타입에 대한 이니셜라이저 위임

이니셜라이저는 다른 이니셜라이저를 호출하여 인스턴스 초기화의 일부를 수행할 수 있음

이니셜라이저 위임이라고 하는 이 과정은 여러 이니셜라이저에서의 코드 중복을 방지해줌

이니셜라이저 위임 작동방식과 위임 형식은 값 유형(구조체 및 열거형)과 클래스 유형에 따라 다름

값 유형은 상속을 지원하지 않으므로 비교적 간단함

그러나 클래스는 클래스가 상속하는 모든 저장 프로퍼티에 초기화중에

적절한 값이 할당되도록 추가 책임이 있음

커스텀 이니셜라이저 선언 시 기본 이니셜라이저 혹은 멤버쪽 이니셜라이저는 사용할 수 없다.

→ 이런 제약들은 복잡성을 낮춰주고 의도치 않게 사용되는 것을 방지 함

→ ! 커스텀 이니셜라이저를 사용하면서 기본-멤버쪽 이니셜라이저도 사용하고 싶다면 커스텀 이니셜라이저를 익스텐션에서 구현하면 됨

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

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)

  1. 첫 이니셜라이저 init()을 이용해 초기화를 하면 각 프로퍼티가 기본값을 초기값으로 갖게 됨
  2. 두번째 이니셜라이저를 사용하면 프로퍼티의 값을 지정할 수 있음
  3. 내부에서 다른 초기자인 두번째 이니셜라이저를 사용함. 그래서 특정 수행을 한 후 ( 위의 예제에서는 originX, originY 값 할당 ) 두번째 이니셜라이저에게 초기화를 위임함
let basicRect = Rect()
// basicRect's origin is (0.0, 0.0) and its size is (0.0, 0.0)

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)

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)

/* 공식문서에서 
이니셜라이저를 익스텐션을 이용해 선언하면 1,2 번째 이니셜라이저는 자동으로 생성되고 
익스텐션에 선언한 이니셜라이저도 사용할 수 있음
*/

클래스 상속 및 초기화 (Designated Initializers and Convenience Initializers)

클래스가 수퍼클래스에서 상속하는 모든 프로퍼티를 포함하여 클래스의 모든 저장 프로퍼티는 초기화중에 초기값을 할당 해야함

스위프트는 두 종류의 초기화를 정의 함 (Designated Initializers(지정 초기자), Convenience Initializers(편리한 초기자))

지정 초기자는 클래스의 주 초기자임.

클래스의 모든 프로퍼티를 초기화 함. 클래스 타입은 반드시 한개 이상의 지정 초기자가 있어야 한다)

편리한 초기자는 초기화 단계에서 미리 지정된 값을 사용해서 최소한의 입력으로 초기화 할 수 있도록 해주는 초기자.

편리한 초기자 내에서 반드시 지정 초기자가 호출 되어야 함

→ 말 그대로 최소한 입력으로 클래스를 초기화 하는것이기 때문에 호출은 최소한의 입력으로 하지만 초기자(convenience init) 내부에서는 주 초기자를 호출해서 모든 프로퍼티를 초기화 해줘야 함

지정 및 편리한 초기자 구문

init(parameter) {
	// 초기화 코드
}
// 지정 초기자

convenience init(paramter) {
	// 초기화 코드
}
// 편리한 초기자 ( 앞에 convenience 키워드를 붙여주면 됨 )

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

위에서 잠깐 언급한 것 처럼 CI(편리한 초기자)안에서 DI(지정 초기자)를 호출해야 한다고 했다.

자세히 살펴보자

초기자간 위임 호출에 대해 세가지 규칙을 적용한다.

  1. 지정 초기자는 직계 superclass 에서 지정 초기자를 호출해야 함
  2. 편리한 초기자는 동일한 클래스에서 다른 초기자를 호출해야함
  3. 편리한 초기자는 궁극적으로 지정 초기자를 호출해야 함

→ 간단한 기억법

  • 지정 초기자는 항상 위임을 superclass로 해야함
  • 편리한 초기자는 반드시 위임을 같은 레벨에서 해야 함

[설명]

superclass

→ 위의 수퍼클래스는 1개의 DI , 2개의 CI를 가지고 있음

→ 오른쪽에서 부터 CI 는 CI 를 (규칙 2) 호출, 그 CI 는 DI 를 다시 호출함 (규칙 3).

subclass

→ 위의 서브클래스는 2개의 DI, 1개의 CI를 가지고 있음

→ CI는 1개의 DI를 호출 (규칙 2, 3)

→ DI는 superclass 의 DI 호출 ( 규칙 1)

규칙은 클래스의 이니셜라이저 구현을 작성하는 방법에만 영향을 미치고 각 클래스의 인스턴스를

만드는 방법에는 영향을 주지 않음

위의 예제는 전의 예제보다 더 복잡한 클래스 계층 구조를 보여줌. 하지만 결국 규칙을 따라가다 보면

쉽게 이해가 가능한 그림이다.

2단계 초기화

스위프트에서 클래스 초기화는 2단계로 진행 됨.

1단계는 각 저장된 프로퍼티는 초기값으로 초기화 됨

→ 모든 저장 프로퍼티의 상태가 결정 → 2번째 단계 시작

2단계는 새로운 인스턴스의 사용이 준비됐다고 알려주기 전에 저장된 프로퍼티를 커스터마이징 하는 단계

스위프트의 컴파일러는 2단계 초기화가 에러없이 끝나는 것을 보장하기 위해

4단계 안전확인을 함

→ 화살표 내용은 내가 이해한 내용임

  1. 지정 초기자는 상위 클래스 초기자에 위임하기 전에 모든 프로퍼티를 초기화 해야함→ 서브클래스에서 슈퍼클래스로 초기자를 위임할 때, 서브클래스의 모든 프로퍼티를 초기화 해야한다는 말인 듯
  2. 지정 초기자는 반드시 다른 초기자로 넘기기 전에 소유하고 있는 모든 프로퍼티를 초기화 해야함
  3. 지정 초기자는 반드시 상속된 값을 할당하기 전에 superclass의 초기자로 위임을 넘겨야함→ 슈퍼클래스의 초기자를 호출 하고 상속된 값을 수정해야 한다는 말 인듯
  4. → 상속된 값을 할당하고 슈퍼 클래스의 초기자를 나중에 호출하면 슈퍼클래스에서 다시 초기화가 일어나 초기값으로 되돌아가기 때문
  5. 그렇지 않으면 상속된 값이 superclass 초기자에 의해 덮어 쓰여지게 됨
  6. 편리한 초기자는 반드시 어떤 프로퍼티를 할당하기 전에 다른 초기자로 위임을 넘겨야 함→ 2번과 비슷한 내용인 것 같은데 프로퍼티를 할당하려면 초기화 작업을 끝내고 해라. 정도로 이해하면 될 듯 ( 먼저 할당하면 다른 초기자에 의해 덮어 씌여지기 때문에 )
  7. 만약 그렇지 않으면 편리한 초기자에 의해 할당된 값을 다른 클래스의 지정 초기자에 의해 덮어 쓰여지게 됨
  8. 초기자는 초기화의 1단계가 끝나기 전에는 self의 값을 참조하거나 어떤 인스턴스 프로퍼티, 메소드 등을 호출하거나 읽을 수 없음→ 첫 단계가 끝날 때 인스턴스가 유효하다고 볼 수 있음
  9. → 클래스의 모든 프로퍼티가 초기화 되기 전까지는 인스턴스가 유효하지 않기 때문에 접근 불가

여기까지 약간 어려울 수 있는데 2단계 초기화를 자세히 살펴보자

1단계

  • 지정 또는 편리한 초기자가 호출 됨
  • 해당 클래스의 새 인스턴스에 대한 메모리가 할당 됨 ( 아직 초기화는 되지 않음 )
  • 해당 클래스에 대한 지정 초기자는 모든 프로퍼티에 값이 있음을 확인함. ( 모든 저장 프로퍼티에 대한 메모리가 초기화 됨 )
  • 지정 초기자는 자신의 저장 프로퍼티에 대해 동일한 작업을 수행하기 위해 슈퍼클래스 초기자에 전달
  • 이것은 클래스 계층의 맨 위에 도달할 때 까지 계속됨
  • 계층의 맨 위에 도달하고 체인의 마지막 클래스가 모든 저장 프로퍼티에 값이 있음을 확인하면 인스턴스의 메모리가 완전히 초기화 된 것으로 간주 → 1단계 완료

2단계

  • 계층의 맨 위에서 다시 작업하면서 계층의 각 지정 초기자에는 인스턴스를 추가로 사용자 지정할 수 있는 옵션이 있음. 초기자는 이제 self 에 접근 가능하고, 프로퍼티 메소드 등에 접근가능함
  • 끝으로, 계층에있는 모든 편리한 초기자는 self 와 함께 인스턴스를 수정할 수 있는 옵션을 가짐

→ 결국 2단계에서 super.init()으로 슈퍼클래스들의 프로퍼티까지 초기화(반복) 하면 self 에 접근할 수 있다는 뜻

→ 초기화가 완료 됐으므로 메소드 호출이나 프로퍼티에 접근해서 커스터마이징 가능 ( 꼭 그럴 필요는 없지만 )

이니셜라이저 상속 및 재정의

스위프트에서는 기본적으로 서브클래스에서 슈퍼클래스의 이니셜라이저를 상속하지 않음

이유는 슈퍼클래스의 이니셜라이저가 무분별하게 상속 돼 복잡하게 돼 서브클래스에서 잘못 초기화 됨을 막기 위함

만약 클래스에서 모든 프로퍼티의 초기 값이 있고 아무런 커스텀 이니셜라이저를 선언하지 않았다면 기본 이니셜라이저 init() 을 사용할 수 있음

슈퍼클래스의 초기자를 오버라이드 하기 위해서는 서브클래스에서 그 초기자에 override 키워드를 붙이고 재정의 해야함

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

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

// 서브클래스에서 슈퍼클래스의 init() 초기자를 오버라이드 한 모습 

자동 초기자 인스턴스

위에서 언급했듯이 서브클래스는 슈퍼클래스의 초기자를 기본적으로 상속 X

그러나 특정 상황에서는 자동으로 상속 받음

많은 상황에서는 직접 초기자를 오버라이드 할 필요가 없음

자동으로 상속받는 규칙

규칙1

  • 서브클래스가 지정초기자를 정의하지 않으면 자동으로 슈퍼클래스의 모든 지정초기자를 상속합니다.

규칙2

  • 서브클래스가 슈퍼클래스의 지정초기자를 모두 구현한 경우(규칙 1의 경우 혹은 모든 지정초기자를 구현한 경우) 자동으로 수퍼클래스의 편리한 초기자를 추가합니다.

지정 초기자와 편리한 초기자의 사용

class Food {
    var name: String
    init(name: String) {
        self.name = name
    }
    convenience init() {
        self.init(name: "[Unnamed]")
    }
}
// 편리한 초기자에서 지정 초기자를 호출되는 형태

let namedMeat = Food(name: "Bacon")
// 지정 초기자를 이용해 인스턴스 생성하는 예제
// namedMeat's name is "Bacon"

let mysteryMeat = Food()
// 편리한 초기자를 이용해 인스턴스 생성하는 예제
// mysteryMeat's name is "[Unnamed]"

Food클래스를 서브클래싱해서 생성한 클래스에서 슈퍼클래스의 지정초기자를 서브클래스의 편리한

초기자로 오버라이딩한 예시

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

let oneMysteryItem = RecipeIngredient() // 자동 초기자로 슈퍼클래스의 CI를 사용
let oneBacon = RecipeIngredient(name: "Bacon") // 
let sixEggs = RecipeIngredient(name: "Eggs", quantity: 6)

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

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

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

RecipeIngredient 클래스가 init(name: String) 을 편리한 초기자로 제공한다 하더라도, 슈퍼클래스의 지정초기자를 모두 제공하고 있다.

그러므로 RecipeIngredient는 자동적으로 슈퍼클래스의 모든 편리한 초기자를 상속받음.(규칙2)

그래서 let oneMysteryItem = RecipeIngredient() 가 가능한 것..

이 부분이 좀 많이 헷갈렸음..

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

var breakfastList = [
    ShoppingListItem(),
    ShoppingListItem(name: "Bacon"),
    ShoppingListItem(name: "Eggs", quantity: 6),
]
breakfastList[0].name = "Orange juice"
breakfastList[0].purchased = true
for item in breakfastList {
    print(item.description)
}
// 1 x Orange juice ✔
// 1 x Bacon ✘
// 6 x Eggs ✘

위의 예제에서

ShoppingListItem 은 RecipeIngredient 를 상속 받았음

ShoppingListItem 은 새로 생성한 모든 프로퍼티에 대해 기본값이 있고, 새로운 초기자를 정의하지 않았기 때문에 자동으로 모든 지정초기자와

옵셔널 초기자 ( 실패 가능한 초기자 )

초기화 과정 중에 실패할 가능성이 있는 초기자를 init? 으로 선언해 표시 할 수 있음

초기자는 이름이 따로 있는 것이 아니라 파라미터로 구분하기 때문에

실패 가능한 초기자와 실패 불가능한 초기자를 같은 파라미터 타입과 이름으로 동시에 사용 불가

실패 가능한 초기자는 반환값으로 옵셔널 값을 생성

초기화에 실패하는 부분에서 nil 을 반환하는 코드를 작성해 초기화 실패를 나타낼 수 있음

엄밀히 말하면 초기자는 값을 반환 하지 않음. 그래서 비록 초기화 실패를 나타내기 위해 return nil 을 사용하지만, init이 성공하는 경우는 return 키워드를 사용하지 않음

다음 코드는 숫자형을 위해 정의돼 있는 실패 가능한 초기자를 사용한 예제임

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"

다음 코드는 초기자에 입력값이 없으면 초기화 실패가 발생하도록 구현한 예제임

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

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

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

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

if anonymousCreature == nil {
    print("The anonymous creature could not be initialized")
}
// Prints "The anonymous creature could not be initialized"

/* 공식문서에서 
빈것과 nil 은 다름. 예를들어 "" 는 isEmpty(값이 비었다) 는 true 이지만, nil 인 상태는 아님
*/

열거형에서 사용하는 실패 가능한 초기자

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값을 초기자 인저로 넣어 초기화에 사용가능 함

let someEnum = SomeEnum(rawValue: value) 이런식으로 잓

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 name이 없거나 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)
    }
}

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

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"

실패 가능 초기자의 오버라이딩

슈퍼클래스의 실패가능한 초기자를 서브클래스에서 실패불가능한 초기자로 오버라이딩 할 수 있음

이 방법을 이용해 실패불가능한 초기자를 생성 가능 함

실패가능한 초기자를 실패 불가능한 초기자에서 오버라이드 할 수 있지만, 그 반대는 불가 함

class Document {
    var name: String?
    // this initializer creates a document with a nil name value
    init() {}
    // this initializer creates a document with a nonempty name value
    init?(name: String) {
        if name.isEmpty { return nil }
        self.name = name
    }
}

class AutomaticallyNamedDocument: Document {
    override init() {
        super.init()
        self.name = "[Untitled]"
    }
    override init(name: String) {
        super.init()
        if name.isEmpty {
            self.name = "[Untitled]"
        } else {
            self.name = name
        }
    }
}
// Auto~~ 클래스 에서는 Document를 서브 클래싱함. 
// 기본 초기자와 지정 초기자를 실패 불가능한 초기자로 오버라이딩

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

// 기본 초기자를 오버라이딩, 그 값이 옵셔널 값을 갖지 않도록 느낌표로 강제 언래핑  

실패 가능한 init! 초기자

실패가능한 초기자 init?을 init!로 오버라이딩 할 수 있으며, 위임해서 사용 가능

필수 초기자

모든 서브클래스에서는 반드시 구현해야하는 초기자에는 아래 예제와 같이 required키워드를 붙여줌

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

필수 초기자를 상속받은 서브클래스에서도 반드시 reqiured 키워드를 붙여서 다른 서브클래스에게도

이 초기자는 필수 초기자라는 것을 알려야 함.

→ 가끔 UIKit에서 서브클래스를 만들 때, required init() 생성하라고 컴파일러에서 오류를 뱉어댔는데 그 이유가 이거였음.

표시를 해도 꼭 구현할 필요는 없다.

클로저나 함수를 이용해 기본 프로퍼티 값 설정

기본 값 설정이 복잡하면 클로저나 함수로 값을 초기화 할 수 있음

기본 값을 지정하기 위해 클로저를 사용하는 형태의 코드

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

클로저를 초기자에서 사용하면 클로저안에 self나 다른 프로퍼티를 사용할 수 없음

→ 클로저가 실행될 시점에 다른 프로퍼티는 초기화가 다 끝나지 않았기 때문

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"