Swift 공식 문서

Initialization

GayoonKim 2024. 1. 3. 20:41

타입의 저장된 프로퍼티에 초기 값을 설정하고 초기 설정을 수행한다.

 

인스턴스에 각 저장된 프로퍼티에 초기 값을 설정하고 새로운 인스턴스가 사용할 준비가 되기 전에 다른 설정이나 초기화를 수행하는 것을 포함한다.

 

새로운 인스턴스를 생성하기 위해 특수 메서드를 호출하는 것처럼 초기화 구문(initializers)을 정의하여 초기화를 구현한다. Objective-C 초기화 구문과 달리 Swift 초기화 구문은 값을 반환하지 않는다. 주요 역할은 처음 사용되기 전에 타입의 새로운 인스턴스가 올바르게 초기화되는 것을 보장하는 것이다.

 

클래스는 클래스 인스턴스가 할당 해제되기 전에 정리를 수행하는 초기화 해제(deinitializer)도 구현할 수 있다.


Setting Initial Values for Stored Properties

클래스와 구조체는 인스턴스가 생성될 때까지 모든 저장된 프로퍼티에 적절한 초기 값을 반드시 설정해야 한다. 

 

초기화 구문 내에서 저장된 프로퍼티에 초기 값을 설정하거나 프로퍼티의 정의 부분으로 기본 프로퍼티 값을 할당할 수 있다.

 

초기화 구문 (Initializers)

특정 타입의 새로운 인스턴스를 생성하기 위해 호출된다.

 

가장 간단한 형식은 init 키워드를 사용하여 작성한다.

init() {
    // perform some initialization here
}

 

화씨 눈금으로 표현된 온도를 저장하는 Fahrenheit 구조체 정의

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"

 

Default Property Values

프로퍼티의 선언의 일부로 기본 프로퍼티 값(default property value)을 지정한다.

 

프로퍼티가 항상 같은 초기 값을 가진다면 초기화 구문 내에서 값을 설정하기보다 기본 값을 제공하는 것이 낫다. 결과는 같지만 기본 값은 프로퍼티의 초기화를 선언에 더 가깝게 연결한다.

 

위 예제의 더 간단한 형식

struct Fahrenheit {
    var temperature = 32.0
}

Customizing Initialization

초기화 중에 상수 프로퍼티 할당으로 초기화 단계를 사용자화 할 수 있다.

 

Initialization Parameters

초기화의 정의의 부분으로 초기화 파라미터(Initialization Parameters)를 제공할 수 있다. 함수와 메서드 파라미터로 동일한 기능과 구문을 가지고 있다.

 

섭씨로 표현되는 온도를 저장하는 Celsius 구조체

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

 

Parameter Names and Argument Labels

함수와 메서드의 파라미터와 마찬가지로 초기화 파라미터는 초기화 구문 내에서 사용하는 파라미터명과 초기화 구문을 호출할 때 사용하는 인수 라벨 모두 가질 수 있다.

 

초기화 구문은 함수와 메서드처럼 함수 이름을 가지지 않는다. 따라서 초기화 구문의 파라미터의 이름과 타입은 어떤 초기화 구문을 호출해야 하는지 식별하는데 특히 중요한 역할을 한다. 이러한 이유로 스위프트 생성자는 개발자가 따로 인수 레이블을 생성하지 않으면 자동적으로 모든 매개 변수에 대한 인수 레이블을 제공한다.

 

초기화 때 파라미터 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
    }
}

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)
// ERROR!!!! - 인자이름 안써서 에러가 남
// this reports a compile-time error - argument labels are required

 

Initializer Parameters Without Argument Labels

인수 라벨 사용을 원치 않을 경우 명시적으로 인수 라벨 대신에 언더바(_)를 작성해 기본 동작을 재정의할 수 있다.

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

초기화 동안 값을 설정할 수 없거나 추후에 값없음을 가질 수 있는 값을 옵셔널로 선언해 사용할 수 있다. 옵셔널 프로퍼티는 자동으로 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."

 

Assigning Constant Properties During Initialization

생성자에서는 상수 프로퍼티에 값을 할당하는 것도 가능하다. 값이 할당되면 더 이상 수정할 수 없다.

 

클래스 인스턴스의 경우 초기화하는 동안 상수 프로퍼티를 수정하는 것은 해당 프로퍼티를 도입한 클래스에 의해서만 가능하다. 하위 클래스에서는 수정할 수 없다.

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

모든 프로퍼티에 대한 기본 값을 제공하고 적어도 하나의 초기화 구문을 제공하지 않는 모든 구조체 또는 클래스에 대해 모든 프로퍼티를 기본 값으로 초기화하는 기본 초기화 구문을 제공한다.

 

쇼핑 리스트에 있는 항목의 이름, 수량, 그리고 구매 상태를 캡슐화하는 ShoppingListItem 클래스

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

 

Memberwise initializers for Structure Types

구조체 타입은 사용자화 초기화 구문을 제공하지 않으면 자동적으로 멤버별 초기화 구문을 받는다. 기본 초기화 구문과 다르게 기본 값을 가지지 않는 저장된 프로퍼티 라도 멤버별 초기화 구문을 받는다.

 

새로운 인스턴스의 프로퍼티를 위환 초기화 값은 이름으로 멤버별 초기화 구문으로 전달될 수 있다.

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

초기화 구문은 인스턴스의 초기화의 부분을 수행하기 위해 다른 초기화 구문을 호출할 수 있다. 초기화 구문 위임(initializer delegation)은 여러 생성자에서 코드가 중복되는 것을 막는다.

 

동작하는 방식과 허용하는 위임 형식에 대한 규칙은 값 타입과 클래스 타입에 따라 다르다. 값 타입(구조체, 열거형)은 상속을 지원하지 않는다. 따라서 자신이 제공하는 생성자에게만 위임할 수 있기 때문에 비교적 구조가 간단하다. 반대로, 클래스는 상속할 수 있기 때문에 상속하는 모든 저장된 프로퍼티가 초기화 중에 적절한 값이 할당되도록 해야 한다.

 

값 타입의 경우 self.init을 통해 새로운 생성자를 정의할 때 다른 생성자를 참조할 수 있다. 생성자를 정의할 때만 self.init을 호출할 수 있다. 

 

값 타입(구조체, 열거형)의 경우 사용자 정의 생성자를 정의하면 기 타입에 대한 기본 생성자 또는 구조체의 경우 멤버별 생성자에 대해 더 이상 접근할 수 없다. 이 제약은 생성자를 정의했지만 이를 사용하지 않고 기본 생성자를 사용하여 발생하게 되는 오류를 방지할 수 있다. default 생성자도 사용하고 사용자 정의 생성자도 사용하고 싶다면 사용자 정의 생성자를 extension으로 선언하면 된다.

 

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()
    • 자체 사용자 정의 생성자가 없으면 기본 생성자와 기능적으로 동일하다. origin과 size 프로퍼티는 Point(x: 0.0, y: 0.0)과 Size(width: 0.0, height: 0.0)의 기본 값으로 초기화되는 Rect 인스턴스 반환
  • init(origin:size:)
    • 멤버별 초기화 구문과 기능적으로 동일
    • origin과 size 인수 값을 적절한 저장된 프로퍼티에 할당
  • init(center:size:)
    • center점과 size 값을 기반으로 원점을 계산하는 것으로 시작한다. 그런 다음 새로운 원점과 크기로 저장하는 init(origin:size) 초기화 구문을 호출 또는 위임한다.
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)

 


Class Inheritance and Initialization

상위 클래스로부터 상속받은 모든 프로퍼티를 포함한 모든 클래스의 저장 프로퍼티는 초기화 중에 반드시 초기 값이 할당되어야 한다.

 

모든 저장된 프로퍼티가 초기 값을 받을 수 있도록 클래스 타입에 대한 두 가지의 생성자를 정의한다.

  • 지정 생성자(designated initializer)
  • 편의 생성자(convenience initializer)

 

Designated Initializers and Convenience Initializers

지정된 생성자(designated initializers)는 클래스의 기본적인 생성자이다. 해당 클래스에 의해 도입된 모든 프로퍼티를 완벽하게 초기화하고 적절한 상위 클래스의 생성자를 호출해 상위 클래스의 프로퍼티들도 초기화한다. 클래스에는 하나의 지정된 생성자가 있는 것이 일반적이다. 지정된 생성자가 호출되는 지점은 상위 클래스 체인까지 계속되는 "funnel" 지점이다.

 

모든 클래스는 적어도 하나의 지정된 생성자가 있어야 한다. 이러한 요구 사항은 자동 생성자 상속(Automatic Initializer Inheritance)에서 설명된 대로 상위 클래스에서 하나 이상의 지정 생성자를 상속하여 충족된다.

 

편의 생성자(convenience initializer)는 클래스에 대해 생성자를 지원하는 보조 생성자이다. 클래스의 특수한 인스턴스를 만들 때 사용하기 위해 편의 초기화를 사용할 수 있다. 만약 필요하지 않은 경우 굳이 제공할 필요는 없다.

 

Syntax for Designated and Convenience Initializers

지정된 생성자는 값 타입에 대한 간단한 생성자와 동일한 방법

init(<#parameters#>) {
   <#statements#>
}

 

편의 생성자도 동일하지만 init 키워드 전에 convenience 작성

convenience init(<#parameters#>) {
   <#statements#>
}

 

Initializer Delegation for Class Types

지정 생성자와 편의 생성자 사이의 관계를 단순화하기 위해 Swift는 초기화 사이의 위임 호출에 대한 3가지 규칙을 적용한다.

  • 지정 생성자는 상위 클래스로부터 지정된 초기화 구문을 호출해야 한다.
  • 편의 생성자는 같은 클래스로부터 다른 생성자를 호출해야 한다.
  • 편의 생성자는 궁극적으로 지정 생성자를 호출해야 한다.

 

위 규칙을 기억하는 단순한 방법

  • 지정 생성자는 항상 위로 위임
  • 편의 생성자는 항상 옆으로 위임

 

상위 클래스에서 하나의 편의 생성자는 다른 편의 생성자를 호출하고 차례로 지정 생성자를 호출한다. 상위 클래스는 더 이상 상위 클래스를 가지고 있지 않기 때문에 규칙 1은 적용되지 않는다.

 

하위 클래스에서 편의 생성자는 같은 클래스의 생성자만 호출할 수 있으므로 2개의 지정 생성자 중 하나를 호출해야 한다. 지정 생성자 모두 위 규칙 1을 충족하기 위해 상위 클래스의 단일 지정 생성자를 호출해야 한다.

 

이러한 규칙들은 각 클래스의 인스턴스를 생성하는 방법에 영향을 주지 않는다. 오로지 클래스의 생성자 구현을 어떻게 작성해야 하는지만 영향을 준다.

 

더 복잡한 예시

 

Two-Phase Initialization

Swift에서 클래스 초기화는 2단계 프로세스이다. 첫 번째 단계에서는 각 저장된 프로퍼티가 해당 프로퍼티를 도입한 클래스에 의해 초기 값이 할당된다. 모든 저장된 프로퍼티의 초기 상태가 결정되면 두 번째 단계가 시작되고 새 인스턴스가 생성되기 전에 저장 프로퍼티의 값을 바꿀 수 있는 기회가 주어진다.

 

2 단계 초기화 프로세르를 통해 클래스 계층 구조의 각 클래스는 완전한 유연성을 가지게 된다. 프로퍼티 값이 초기화되기 전에는 접근하는 것을 막고 다른 생성자가 예기치 않게 다른 값을 설정하는 것을 막는다.

 

Objective-C의 초기화와 유사하다. 주요 차이점은 Objective-C는 모든 프로퍼티에 0 또는 nil 값을 할당한다. Swift는 사용자 정의 초기 값을 설정할 수 있고 0 또는 nil이 유요효한 기본 값이 아닌 타입에 대처할 수 있다는 점에서 더 유연하다.

 

에러 없이 2단계 초기화가 완료되었는지 확인하기 위해 4가지의 검사를 수행한다.

  • 안전 점검 1
    • 지정 생성자는 상위 클래스 생성자에 위임되기 전에 클래스에 의해 도입된 모든 프로퍼티가 초기화 되었는지 확인한다. 객체의 메모리는 모든 프로퍼티에 값이 있을 때만 완전히 초기화된 것으로 간주한다. 이 규칙을 충족하기 위해 지정된 초기화 구문은 상위 클래스의 생성자가 실행되기 전에 현재 생성 중인 클래스의 모든 프로퍼티에 값을 할당해야 한다.
  • 안전 점검 2
    • 지정 생성자는 상속된 프로퍼티에 값을 할당하기 위해 상위 클래스의 생성자를 실행한 뒤에 값을 할당해야 한다. 그렇지 않으면 상위 클래스의 생성자에 의해 값이 덮어 쓰여진다.
  • 안전 점검 3
    • 편의 생성자는 모든 프로퍼티에 값을 할당하기 전에 다른 생성에 위임해야 한다. 그렇지 않으면 자체 클래스의 생성자에 의해 깊이 덮어 쓰인다.
  • 안전 점검 4
    • 생성자는 초기화 첫 번째 단계가 완료될 때까지 인스턴스 메서드, 인스턴스 프로퍼티, self를 사용할 수 없다.

 

클래스 인스턴스는 첫 번째 단계가 끝날 때까지 완전히 유효하지 않다. 첫 번째 단계가 끝날 때 클래스 인스턴스가 유효한 것으로 판단된 후에만 프로퍼티에 접근할 수 있고 메서드를 호출할 수 있다.

 

위 안전 점검 기반 2 단계 초기화 수행 방식은 다음과 같다.

  • 1 단계
    • 지정 또는 편의 생성자는 클래스에 의해 호출된다.
    • 클래스의 새로운 인스턴스에 대한 메모리가 할당된다. 메모리는 아직 초기화되지 않았다.
    • 지정 생성자는 해당 클래스의 모든 프로퍼티에 값이 있음을 확인한다. 저장된 프로퍼티에 대한 메모리는 초기화된다.
    • 지정 생성자는 자체 저장된 프로퍼티에 동일한 작업을 수행하기 위해 상위 클래스 생성자를 호출한다.
    • 최상위 클래스에 도달할 때까지 계속한다.
    • 최상위 클래스에 도달한 뒤 최상위 클래스의 모든 프로퍼티에 값이 있는지 확인하며 인스턴스의 메모리가 완전히 초기화된 것으로 보고 1 단계를 종료한다.
  • 2 단계
    • 최상위 클래스에서 다시 내려가면서 클래스에 정의된 지정 생성자는 인스턴스의 값을 다시 지정할 수 있는 옵션이 있다. 생성자는 이제 self로 접근할 수 있으며 프로퍼티를 수정할 수 있고 인스턴스 메서드를 호출하는 등 작업을 수행할 수 있다.
    • 클래스들의 편의 생성자는 인스턴스를 커스터마이징하고 자체적으로 작업을 수행할 수 있다.

 

가장 하위 클래스와 상위 클래스에 대한 초기화 호출을 찾는 방법 - 1 단계

 

이 예제에서 초기화는 하위 클래스의 편의 생성자를 호출하며 시작된다. 편의 생성자는 아직 프로퍼티를 수정할 수 없으므로 동일 클래스의 지정 생성자를 호출한다. 지정 생성자는 안전 점검 1에 따라 하위 클래스의 모든 프로퍼티에 값이 있는지 확인한 뒤 상위 클래스의 지정 생성자를 호출하여 체인까지 초기화를 계속한다. 상위 클래스의 지정 생성자는 상위 클래스의 모든 프로퍼티가 값을 가지고 있는지 확인한다. 초기화할 상위 클래스가 없으므로 추가 위임이 필요 없다. 상위 클래스의 모든 프로퍼티가 초기 값을 가지자마자 메모리는 완전히 초기화되었다고 간주하고 첫 번째 단계가 완료된다.

 

초기화의 2 단계

 

상위 클래스는 지정 생성자는 이제 인스턴스를 추가로 사용자화 할 수 있는 기회(필수는 아님)를 가진다. 상위 클래스의 지정 생성자가 완료되면 하위 클래스의 지정 생성자가 사용자 정의(필수는 아님)를 수행할 수 있다. 그 후에는 편의 생성자가 추가적인 사용자 정의를 수행할 수 있게 된다.

 

Initializer Inheritance and Overriding

Swift의 하위 클래스는 상위 클래스의 생성자를 상속하지 않는다. 상위 클래스의 생성자가 무분별하게 상속되어 복잡하게 돼 하위 클래스에서 이것들이 잘못 초기화되는 것을 막기 위함이다. 상위 클래스의 생성자는 특정 상황에서 상속되지만 안전하고 적절한 경우에만 상속된다.

 

상위 클래스 지정 생성자와 일치하는 하위 클래스 생성자를 작성할 때 지정 생성자 재정의를 효과적으로 제공한다. 따라서 하위 클래스 생성자 정의 전에 override 키워드를 작성해야 한다. 자동으로 제공된 기본 생성자를 재정의하는 경우에도 동일하다.

 

재정의 된 프로퍼티, 메서드, 그리고 서브 스크립트와 동일하게 override 키워드가 있으면 상위 클래스에 재정의 할 일치하는 지정 생성자 구문이 있는지 확인하고 재정의 할 생성자 파라미터가 의도한대로 지정되었는지 확인하도록 한다. 편의 생성자 구문이라도 상위 클래스 지정된 생성자를 재정의할 떄 항상 override 수식어를 작성한다.

 

반대로 상위 클래스에 편의 생성자와 일치하는 하위 클래스 생성자를 작성하는 경우 해당 상위 클래스 편의 생성자는 하위 클래스에서 직접적으로 호출될 수 없다. 따라서 하위 클래스는 상위 클래스 생성자 재정의를 제공하지 않는다. 결과적으로 상위 클래스 편의 생성자와 일치하는 구현을 제공할 때 override 키워드를 작성하지 않는다.

 

하위 클래스에서 생성자 재정의 예제

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

 

따로 생성자를 생성하지 않았기 떄문에 기본 생성자를 사용하게 된다. 이러한 기본 생성자는 클래스에서 항상 지정 생성자이다.

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

 

이번에는 Bicycle 이라는 Vehicle 하위 클래스

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

 

만약 하위 클래스의 생성자가 초기화 프로세스의 2 단계에서 사용자 지정 생성자가 없고, 상위 클래스의 지정 생성자에 매개 변수가 없을 땐 하위 클래스의 저장 프로퍼티에 값을 할당한 후에 super.init() 호출을 생략할 수도 있다. 상위 클래스의 생성자가 비동기적이라면 명시적으로 await super.init()을 작성해야 한다.

 

아래 예제는 Hoverboard라는 Vehicle의 다른 하위 클래스 정의

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

 

color 프로퍼티만 설정한다. 이 생성자는 super.init()을 암시적으로 호출하여 초기화 프로세스를 완료한다. 하위 클래스는 상속된 변수 프로퍼티의 값은 수정할 수 있지만 상속된 상수 프로퍼티의 값은 수정할 수 없다.

 

Automatic Initializer Inheritance

하위 클래스는 기본적으로 상위 클래스 생성자를 상속하지 않는다. 그러나 특정 조건이 충족되면 상위 클래스 생성자는 자동으로 상속된다.

 

하위 클래스에 도입한 모든 프로퍼티에 기본 값을 제공하면 아래 2가지 규칙이 적용된다.

  • 규칙 1
    • 하위 클래스가 지정 생성자를 정의하지 않으면 자동으로 상위 클래스 지정 생성자를 모두 상속한다.
  • 규칙 2
    • 하위 클래스가 규칙 1에 따라 상속하거나 정의의 부분으로 사용자 정의 구현을 제공하여 모든 상위 클래스 지정 생성자 구현을 제공한다면 모든 상위 클래스 편의 생성자를 자동으로 상속한다.

 

이러한 규칙은 하위 생성자가 편의 생성자를 추가할 때도 적용된다. 하위 클래스는 규칙 2를 만족하는 부분으로 하위 클래스 편의 생성자로 상위 클래스 지정 생성자를 구현할 수 있다.

 

Designated and Convenience Initializers in Action

지정, 편의, 그리고 자동 상속 생성자의 동작 예제

식품의 이름을 캡슐화 하는 Food 클래스

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

 

Food 클래스 생성자 체인

 

클래스는 기존 멤버별 생성자를 가지지 않기 때문에 Food 클래스는 name 이라는 하나의 인수를 가지는 지정 생성자를 제공한다. 이 생성자는 특정 이름으로 새로운 Food 인스턴스를 생성하기 위해 사용될 수 있다.

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

 

Food 클래스는 상위 클래스를 가지고 있지 않으므로 init(name: String) 생성자는 초기화를 완료하기 위해 super.init()을 호출할 필요가 없다.

 

Food 클래스는 인수가 없는 init()의 편의 생성자도 제공한다. [Unnammed]의 name 값으로 init(name: String)으로 위임하여 새로운 음식을 위한 기본 이름을 제공한다.

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

 

계층도에 두 번째 클래스는 RecipeIngredient라는 Food의 하위 클래스이다. 요리 레시피 재료를 모델링한다. quantity라는 Int 프로퍼티를 도입하고 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 클래스 생성자 체인

 

새로운 인스턴스에 모든 프로퍼티를 채울 수 있는 init(name: String, quantity: Int)인 하나의 지정 생성자를 가지고 있다. RecipeIngredient에 도입된 새로운 프로퍼티인 quantity 프로퍼티에 전달된 quantity 인수를 할당하는 것으로 시작한다. 그 후에 Food 클래스에 init(name: String) 생성자 구문으로 위임한다.

 

이름으로만 새로운 인스턴스를 생성하기 위해 사용되는 init(name: String)인 편의 생성자도 정의한다. 이 편의 생성자는 명시적인 양 없이 생성되는 모든 인스턴스에 대해 1의 양으로 가정한다. 이러한 편의 생성자 구문은 인스턴스를 더 빠르고 편리하게 생성하도록 하고 여러 개의 단일 양의 인스턴스를 생성할 때 코드 중복을 피할 수 있다.

 

init(name: String) 퍈의 생성자는 Food에 init(name: String) 지정 생성자와 같은 파라미터를 가지고 있다. 상위 클래스의 지정 생성자를 재정의 하기 때문에 override 키워드를 붙여야 한다.

 

RecipeIngredient는 상위 클래스의 지정 생성자의 모든 구현을 제공한다. 따라서 자동으로 모든 상위 클래스의 편의 생성자도 상속한다.

 

세 가지 생성자 모두 새로운 RecipeIngredient 인스턴스를 생성하기 위해 사용될 수 있다.

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

 

세 번째와 마지막 클래스는 ShoppingListItem이라는 RecipeIngredient의 하위 클래스이다. 쇼핑 리스트에 표기되는 레시피 재료를 모델링 한다.

 

쇼핑 리스트에 모든 아이템은 "미구매"로 시작한다. 이를 위해 기본 값이 false인 purchased라는 Bool 프로퍼티를 도입한다. ShoppingListitem 인스턴스의 설명을 제공하는 연산 description 프로퍼티도 추가한다.

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

 

도입한 모든 프로퍼티에 대해 기본 값을 제공하고 생성자 자체를 정의하지 않기 때문에 자동으로 상위 클래스의 모든 지정 생성자와 편의 생성자를 상속한다.

 

세 클래스의 모든 생성자 체인

 

상속된 세 가지 생성자를 모두 사용하여 새로운 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 ✘

 

breakfastList는 3개의 새로운 ShoppingListitem 인스턴스를 포함한다. 배열의 타입은 [ShoppingListItem]으로 추론된다.


Failable Initializers

유효하지 않은 초기화 파라미터 값, 외부 리소스 부재 또는 초기화 성공을 방해하는 기타 다른 조건에 의해 실패될 수 있다.

 

실패할 수 있는 초기화 조건을 대처하려면 실패 가능한 초기화 클래스, 구조체, 또는 열거형 일부로 정의한다. init 키워드 뒤에 ?를 표기하여 실패 가능한 생성자를 작성한다 (init?). 동일한 파라미터 타입과 이름으로 실패 가능한 생성자와 실패 불가능한 생성자를 정의할 수 없다.

 

실패할 가능한 생성자는 초기화 하는 타입의 옵셔널 값을 생성한다. 실패 가능한 생성자 내에 return nil을 작성하여 초기화가 실패할 수 있는 지점을 나타낸다.

 

엄밀히 말하면 생성자는 값을 반환하지 않는다. 오히려 생성자의 역할은 초기화가 끝날 때까지 self가 완전하고 정확하게 초기화 되도록 하는 것이다. 초기화 실패를 위해 return nil을 작성하지만 성공을 위해 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"

 

아래 예제는 species라는 String 프로퍼티 상수를 가진 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")

if let giraffe = someCreature {
    print("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"

 

"Giraffe" 대신 ""와 같은 빈 문자열 값을 검사하는 것은 옵셔널 String에 값이 없음을 나타내는 nil을 검사하는 것과 다르다. 위 예제에서 빈 문자열("")은 유효하고 옵셔널이 아닌 String이다. 그러나 species 프로퍼티에 값으로 빈 문자열을 갖는 것은 동물에 대해 적절하지 않다. 이 제한을 모델링 하기위해 실패 가능한 생성자는 빈 문자열을 찾으면 초기화에 실패한다.

 

Failable Initializers for Enumerations

하나 이상의 파라미터를 기반으로 적절한 열거형 케이스를 선택하기 위해 실패 가능한 생성자를 사용할 수 있다. 이 생성자는 제공된 파라미터가 적절한 열거형 케이스와 일치하지 않으면 실패할 수 있다.

 

온도 표기를 위해 가능한 3가지 상태를 가지는 TemperatureUnit 열거형

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

 

Failable Initializers for Enumerations with Raw Values

원시값을 가진 열거형은 적절한 원시값 타입의 rawValue라는 파라미터를 가지고 일치하는 값을 찾으면 일치하는 열거헝 케이스를 선택하거나 일치하는 값이 없으면 초기화 실패를 나타내는 실패 가능한 생성자를 자동으로 받는다.

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

 

Propagation of Initialization Failure

클래스, 구조체, 또는 열거형의 실패 가능한 생성자는 같은 클래스, 구조체, 또는 열거형에 다른 실패 가능한 생성자로 위임할 수 있다. 초기화 실패를 유발하는 다른 생성자에 위임하면 전체 초기화 프로세스는 즉시 실패하고 더 이상 초기화 코드는 실행되지 않는다.

 

실패 가능한 생성자는 실패 불가능한 생성자에도 위임할 수 있다. 실패하지 않는 기존 초기화 프로세스에 실패 상태를 추가해야 하는 경우에 사용할 수 있다.

 

CertItem이라는 Product의 하위 클래스를 정의한다. CertItem 클래스는 온라인 쇼핑 카트에 있는 상품을 모델링한다.

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

 

CertItem에 대한 실패 가능한 생성자는 quantity 값에 1 또는 그 이상의 값을 받는지 확인하는 것으로 시작한다. quantity 값이 유효하지 않으면 전체 초기화 프로세스는 즉시 실패하고 더 이상 초기화 코드는 실행되지 않는다. 마찬가지로 Product에 대한 실패 가능한 생성자는 name 값을 검사하고 name이 빈 문자열이라면 즉시 초기화 프로세스는 실패한다.

 

이름을 가지고 1 또는 그 이상의 양을 가진 CertItem 인스턴스 생성

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

 

0의 quantity 값을 가진 CertItem 인스턴스를 생성하면 초기화 실패

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 값을 가진 CertItem 인스턴스를 생성하면 상위 클래스 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

다른 생성자처럼 상위 클래스의 실패 가능한 생성자를 하위 클래스에서 재정의 할 수 있다. 또는 하위 클래스 실패 불가능한 생성자로 상위 클래스 실패 가능한 생성자를 재정의 할 수 있다. 이를 통해 상위 클래스의 초기화가 실패 하더라도 초기화를 실패할 수 없는 하위 클래스를 정의할 수 있다.

 

실패 가능한 상위 클래스 생성자를 실패 불가능한 하위 클래스 생성자로 재정의하면 상위 클래스 생성자로 위임하는 방법은 실패 가능한 상위 클래스 생성자 값을 강제로 언래핑 하는 것 뿐이다.

 

실패 가능한 생성자를 실패 불가능한 생성자로 재정의 할 수 있지만 그 반대는 불가능하다.

 

비어 있지 않는 문자열 값이나 nil은 가능하지만 빈 문자열은 불가능한 name 프로퍼티를 가지고 초기화 될 수 있는 문서를 모델링하는 Docuemnt라는 클래스

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

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

 

상위 클래스의 실패 가능한 init(name:) 생성자를 실패 불가능한 init(name:) 생성자로 재정의 한다. 빈 문자열 케이스를 상위 클래스와 다르게 처리하므로 생성자가 실패할 필요가 없으므로 대신 실패 불가능한 생성자 버전을 제공한다.

 

하위 클래스의 실패 불가능한 생성자의 부분으로 상위 클래스의 실패 가능한 생성자를 호출하기 위해 생성자에서 강제 언래핑을 사용할 수 있다. 예를 들어, 아래의 UntitledDocument 하위 클래스는 항상 "[Untitled]" 이름을 가지고 상위 클래스 초기화 동안에 실패 가능한 init(name:) 생성자를 사용한다.

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

 

이 경우 상위 클래스 init(name:) 생성자가 이름으로 빈 문자열로 호출 된 경우 강제 언래핑 작업을 통해 런타임 에러가 발생한다. 그러나 문자열 상수로 호출되기 때문에 생성자는 실패하지 않은 것을 알 수 있으므로 이 경우 런타임 에러가 발생하지 않는다.

 

The init! Failable Initializer

적절한 타입의 암시적으로 언래핑된 옵셔널 인스턴스를 생성하는 실패 가능한 생성자를 정의할 수 있다. 물음표 대신에 init 키워드 뒤에 !를 위치 시키면 된다 (init!).

 

init?에서 init!으로 또는 init?를 init!로 또는 그 반대로 재정의 할 수 있다. init에서 init!으로 위임할 수도 있지만 그렇게 하면 init! 생성자에 의해 초기화가 실채한다.


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

 

상속된 생성자로 요구사항을 충족할 수 있으면 필수 생성자에 명시적 구현을 제공하지 않아도 된다.


Setting a Default Property Value with a Closure or 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
    }()
}

 

클로저의 중괄호 끝에는 빈 소괄호 쌍이 온다. 이것은 Swift가 클로저를 즉시 실행하도록 한다. 만약 소괄호를 생략한다면 클로저의 반환 값이 아닌 프로퍼티에 클로저 자체를 할당하려고 한다.

 

프로퍼티를 초기화하기 위해 클로저를 사용하면 클로저가 실행될 때 아직 다른 인스턴스는 초기화 되지 않았다는 것을 기억해야 한다. 해당 프로퍼티에 기본 값이 있더라도 클로저 내에서 다른 프로퍼티 값에 접근할 수 없다. 또한 암시적으로 self 프로퍼티를 사용하거나 인스턴스 메서드를 호출할 수 없다.

 

체스 게임을 위한 보드를 모델링하는 Chessboard 구조체 정의

 

게임 보드를 표현하기 위해 Chessboard 구조체는 64개의 Bool 값 배열인 boardColors라는 하나의 프로퍼티를 가진다. 배열에 true는 검은색 사각형을 표시하고 false는 하얀색 사각형을 표시한다. 배열의 첫 번째는 죄측 상단을 나타내고 마지막 항목은 우측 하단을 나타낸다.

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

 

클로저는 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 공식 문서' 카테고리의 다른 글

Optional Chaining  (1) 2024.01.05
Deinitialization  (1) 2024.01.04
Inheritance  (1) 2023.12.31
Subscripts  (0) 2023.12.29
Methods  (1) 2023.12.28