티스토리 뷰

반응형

이번 글에서는 Swift 공식 문서의 14번째 단원인 Initialization을 읽고 정리한 글을 쓰려고 한다.

Apple Swift 공식 문서 14단원 - Initialization

Initialization

Initialization(생성자)는 클래스, 구조체, 열거형에서 인스턴스를 준비하기 위한 과정이다. 이러한 과정은 인스턴스의 프로퍼티들마다 초기값을 설정해주고 새 인스턴스를 사용하기 전에 필요한 설정과 초기화를 수행하는 과정이 포함된다. 각각의 타입에서 새로운 인스턴스를 만들 수 있는 특수한 메서드와 같은 역할을 하는 생성자를 정의하여 사용할 수 있다. Objective-C의 생성자와는 다르게 Swift의 생성자는 값을 반환하지는 않는다. 생성자의 가장 중요한 역할은 새로운 인스턴스가 처음 사용되기 전에 올바르게 초기화되는 것을 보장하는 것이다. 클래스 인스턴스는 인스턴스를 메모리에서 제거하는 deinitializer(소멸자)도 지원한다.


Setting Initial Values for Stored Properties

클래스와 구조체의 저장 프로퍼티는 인스턴스가 생성될 때 반드시 어떠한 값이 존재해야 한다. 즉 저장 프로퍼티를 불확실한 상태로 둘 순 없다는 말이다. 생성자 또는 프로퍼티 선언 시 직접 default 값을 할당햊는 방법으로 저장 프로퍼티의 값을 설정할 수 있다. 이렇게 생성자나 default 값으로 저장 프로퍼티가 설정될 때에는 프로퍼티 옵저버를 호출하지 않는다.

 

Initializers

생성자는 특정 타입의 새로운 인스턴스가 생성될 때 호출된다. 가장 간단한 모양은 생성자가 매개변수가 없는 인스턴스 메서드같이 선언된 것으로 init키워드를 사용하여 정의된다.

init(){
    // perform some initialization here
}

위의 모양으로 선언할 수 있는데 실제 구조체에 생성자를 정의하여 인스턴스 생성시 호출되는지 알아보자

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"

위의 코드를 보면 temperature라는 저장프로퍼티가 있는데 값이 정해져 있지 않다. 따라서 생성자에서 프로퍼티의 초기값을 설정해줘야 한다. 실제 인스턴스를 생성해보면 생성자에서 설정된 32.0이라는 값이 temperature 프로퍼티에 저장된 것을 볼 수 있다.

 

Default Property Values

인스턴스를 생성할 때 저장 프로퍼티의 값을 생성자로 설정할 수도 있지만 default값을 프로퍼티 선언과 동시에 정의할 수도 있다. 이러한 방법은 프로퍼티가 항상 동일한 초기값을 사용하는 경우에 사용하면 좋다. 간단한 예를 보며 프로퍼티에 default값을 초기값으로 설정하는 방법을 이해해보자

struct Fahrenheit {
    var temperature = 32.0
}

Custimizing Initialization

생성자가 수행되는 것을 매개변수, 옵셔널 프로퍼티, 초기화 중 상수 프로퍼티를 지정하여 생성자를 원하는대로 정의할 수 있다.

 

Initialization Parameters

생성자를 정의할 때 타입과 매개변수의 이름을 가지고 생성자 매개변수를 만들 수 있다. 생성자 매개변수는 함수와 메서드의 매개변수와 동일한 문법을 가진다. 예를 보며 방법을 알아보자.

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

위의 코드와 같이 생성자에 매개변수를 제공할 수 있다. 하지만 Celsius 구조체에는 생성자가 두개인것을 볼 수 있다. 두 개의 생성자들은 서로 다른 생성자 매개변수를 가지고 있는데 해당 생성자에 맞게 인스턴스를 생성하면 해당 생성자가 호출된다. 위의 예에서는 init(fromFahrenheit:) 생성자를 호출하면 화씨온도를 섭씨온도로 변환한 값을 보여주고 init(fromKelvin:) 생성자를 호출하면 켈빈 온도를 섭씨온도로 변환한 값을 보여준다.

 

Parameter Names and Argument Labels

함수와 메서드의 매개변수와 같이 생성자 내부에서 사용될 매개변수도 이름과 생성자를 호출할 때 사용될 Argument Label(인수 레이블)을 가질 수 있다. 하지만 생성자는 이름이 없기 때문에 매개변수의 이름과 타입은 생성자를 구분하는데 중요한 역할을 한다. 이러한 이유로 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)

위의 코드에서 Color라는 구조체를 정의했고 프로퍼티는 red, green, blue를 가지고 있다. Color 구조체는 두 개의 생성자를 가지고 있으며 생성자들은 인수 레이블은 정의되어 있지 않고 매개변수 이름만 정해져 있는 것을 볼 수 있다. 하지만 위의 코드를 보면 실제 구조체 인스턴스를 생성시 생성자에서 매개변수의 이름을 인수 레이블로 사용하는 것을 볼 수 있다. 이렇게 자동적으로 인수 레이블이 설정되는 것을 볼 수 있다.

 

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

예를 들어 위의 코드에서 init(_ celsius:) 생성자가 _ 를 사용한 예이다. 이렇게 생성한 생성자를 사용할 땐 그냥 바로 매개변수를 타입만 맞춰서 넣어주면 된다.

 

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

위의 코드에선 response가 옵셔널 타입으로 선언되었다. 옵셔널 타입으로 선언된 프로퍼티는 nil로 초기화가 되기 때문에 생성자에서 초기화를 시켜주지 않아도 오류가 발생하지 않는다. 물론 나중에 값을 저장할 수도 있다.

 

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

위의 코드에서 text 프로퍼티는 상수로 선언되었다. 하지만 init(text:) 생성자에서 초기화가 되기 때문에 오류가 발생하지 않는다.


Default Initializers

Swift에서는 모든 구조체와 클래스에서 모든 프로퍼티에 대해 기본값을 제공하는 default initializer 하나가 제공된다. default 생성자는 새로운 인스턴스의 모든 프로퍼티를 default 값으로 초기화시켜 생성한다.

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

위의 코드의 ShoppingListItem 클래스에는 생성자가 선언되어 있지 않다. 하지만 Swift의 클래스는 하나의 default 생성자를 제공하기 때문에 인스턴스를 생성하면 모든 프로퍼티가 default값을 가지는 인스턴스가 생성되게 된다. 이때 모든 프로퍼티가 default 값을 가지고 있어야 오류가 발생하지 않으며 하나의 프로퍼티라도 default 값이 없다면 오류가 발생하게 된다.

 

Memberwise Initializers for Structure Types

구조체는 생성자를 정의하지 않으면 memberwise initializer(멤버 별 생성자)를 자동으로 받는다. default 생성자와는 다르게 저장 프로퍼티에 default 값이 없을 때 memberwise initializer를 받게 된다. memberwise initializer는 새로운 구조체 인스턴스의 프로퍼티를 초기화하는 간단한 방법이다. 새로운 인스턴스의 프로퍼티에 대한 초기 값은 이름으로 memberwise initializer에 전달된다.

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

위의 코드가 memberwise initializer를 사용한 예이다. Size 구조체에는 생성자가 정의되어있지 않다. 하지만 실제 인스턴스를 생성할 땐 마치 init(width: height:)를 사용한 것처럼 보인다. 이렇게 멤버 프로퍼티의 이름으로 값을 초기화하는 것이 memberwise initializer이다.

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"

위의 코드와 memberwise initializer를 사용할 때 default 값이 정의되어있는 프로퍼티의 경우 생략해줘도 된다.


Initializer Delegation for Value Types

생성자는 다른 생성자를 호출하여 인스턴스 초기화의 일부를 수행할 수 있다. Initializer Delegation(생성자 위임)이라고 하는 이러한 과정은 생성자에서 코드가 중복되는 것을 방지한다.

 

값 타입인지 참조 타입인지에 따라서 이러한 Initializer delegation의 작동 방법이 달라진다. 값 타입 즉 구조체나 열거형에서는 상속을 지원하지 않는다. 따라서 이들의 생성자 delegation은 자신이 제공하는 생성자에게만 위임할 수 있기 때문에 구조가 간단하다. 하지만 클래스는 다른 클래스를 상속할 수 있기 때문에 상속하는 모든 저장 프로퍼티가 초기화 중에 적절한 값이 할당되도록 해야 한다.

 

값 타입에서는 self.init으로 새로운 생성자를 정의할 때 다른 생성자를 참조할 수 있다. self.init은 생성자를 작성할 때만 호출할 수 있다. 값 타입, 즉 구조체나 열거형에서 사용자 정의 생성자를 정의하면 default 생성자를 (구조체의 경우 memberwise 생성자도) 더 이상 접근할 수 없다. 이러한 제약은 생성자를 만들어뒀지만 누군가가 사용할 때 이를 사용하지 않고 기본적으로 제공되는 생성자를 사용하여 발생하게 되는 오류를 방지할 수 있다. 이러한 제약이 싫고 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)
    }
}

위의 코드에서 Rect 구조체에는 세 개의 사용자 정의 생성자가 존재한다. 또한 두 개의 프로퍼티인 origin, size는 다른 구조체 인스턴스 타입인데 이 프로퍼티를 초기화하는 생성자의 매개변수에 해당하는 Size, Point 구조체는 모두 default값이 있기 때문에 다음과 같이 세 가지 방법으로 Rect 구조체의 인스턴스를 생성할 수 있다.

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)

첫 번째 Rect()의 경우 init() 생성자를 사용한 경우이다. default 생성자를 사용하는 경우와 기능적으로 동일하다.
두 번째 Rect(origin: Point(x: 2.0, y: 2.0), size: Size(width: 5.0, height: 5.0))의 경우 init(origin: size) 생성자를 사용한 경우이고 Rect(center: Point(x: 4.0, y: 4.0), size: Size(width: 3.0, height: 3.0))는 init(center: size) 생성자를 사용한 경우이다.


Class Inheritance and Initialization

모든 클래스의 저장 프로퍼티는 인스턴스가 만들어질 때 반드시 초기값이 할당되어있어야 한다. Swift는 클래스의 저장 프로퍼티들이 반드시 값을 가질 수 있도록 두 가지 생성자를 정의했다. 이는 designated initializer, convenience initializer이다.

 

Designated Initializers and Convenience Initializers

Designated Initializers(지정 생성자)는 클래스의 기본적인 생성자이다. 지정 생성자는 클래스의 모든 프로퍼티를 초기화하고 적절한 슈퍼 클래스의 생성자도 호출하여 슈퍼 클래스의 프로퍼티들도 초기화한다. 클래스에는 하나의 지정 생성자가 있는 것이 일반적이다. 지정 생성자가 호출되는 지점은 "funnel"지점으로 초기화 프로세스가 슈퍼 클래스 체인을 계속하는 지점이다.

 

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

Convenience Initializers(편의 생성자)는 클래스의 생성자를 보조하는 역할을 한다. 클래스의 특수한 인스턴스를 만들 때 사용하기 위해 편의 초기화를 정의할 수도 있다. 만약 이러한 편의 생성자가 필요하지 않을 경우 굳이 제공할 필요는 없다.

 

Syntax for Designated and Convenience Initializers

init ( parameters ) {
    statements
}
// designated Initializer

convenience init ( parameters) {
    statements
}
// convenience Initializer

위와 같이 지정 생성자와 편의 생성자를 정의할 수 있다.

 

Initialzer Delegation for Class Types

지정 생성자와 편의 생성자의 관계를 간단하게 하기 위해 Swift는 생성자의 delegation에 다음의 세 가지 규칙을 적용한다.

  • Rule 1
    지정 생성자는 호출 즉시 슈퍼 클래스의 지정 생성자를 호출한다.
  • Rule 2
    편의 생성자는 같은 클래스 내의 다른 생성자를 호출해야 한다.
  • Rule 3
    편의 생성자는 지정 생성자를 호출해야만 한다.

즉 Designated initializer는 항상 delegate Up 해야 하고 Convenience initializer는 항상 delegate across 해야 한다.

위의 그림에서 슈퍼 클래스는 하나의 지정 생성자와 두 개의 편의 생성자를 가지고 있다. 하나의 편의 생성자는 다른 편의 생성자를 호출하고 다시 하나의 지정 생성자를 호출한다. 이는 위의 규칙 2, 3을 만족한다. 슈퍼 클래스 자체에는 추가 수퍼 클래스가 없기 때문에 규칙 1은 적용되지 않는다.


위의 그림에서 서브 클래스는 두 개의 지정 생성자와 하나의 편의 생성자가 있다. 편의 생성자는 동일한 클래스의 생성자만 호출할 수 있기 때문에 지정 생성자만 호출 가능하다. 남은 지정 생성자들은 규칙 1에 의해 수퍼 클래스의 지정 생성자를 호출해야 하기 때문에 그렇게 호출하고 있는 것을 볼 수 있다. 이렇게 규칙 1,2,3을 모두 준수하고 있다.

이러한 규칙은 클래스를 사용할 때 클래스의 인스턴스를 만드는 방법에는 영향을 끼치지 않는다. 오로지 클래스의 생성자 구현을 작성하는 방법에만 영향을 주는 규칙들이다.

 

Two-Phase Initialization

Swift의 클래스의 초기화는 2단계에 거쳐 진행된다. 첫 번째 단계에서는 저장 프로퍼티에 클래스에 의한 초기 값이 할당된다. 모든 저장 프로퍼티에 초기 값이 결정되면 두 번째 단계가 시작되는데, 이 단계에서는 새로운 인스턴스가 생성되기 전에 저장 프로퍼티의 값을 바꿀 수 있는 기회가 주어진다. 이러한 두 단계에 걸친 프로세스를 사용하면 클래스 계층 구조의 각 클래스는 완전한 유연성을 갖게 된다. 이러한 단계는 프로퍼티에 값이 할당되기 전에는 프로퍼티 값에 접근하지 못하게 하고 다른 생성자가 프로퍼티 값을 다른 값으로 설정하지 못하게 한다.

 

Swift 컴파일러는 네 가지의 안전 점검을 수행하여 2단계 초기화가 완료되었는지 확인한다.

  1. 안전 점검 1
    Designated 생성자는 슈퍼 클래스의 생성자가 실행되기 전에 해당 클래스의 모든 프로퍼티에 값을 할당해야 한다. 객체의 메모리는 모든 프로퍼티에 값이 있을 때만 완전히 초기화 된 것으로 간주한다. 이러한 규칙을 만족하려면 수퍼 클래스의 생성자가 실행되기 전에 현재 생성 중인 클래스의 모든 프로퍼티에 값을 할당해야한다.
  2. 안전 점검 2
    Designated 생성자는 상속된 프로퍼티에 값을 할당하고 싶다면 슈퍼 클래스의 생성자를 실행한 뒤에 값을 할당해야한다. 그렇지 않으면 수퍼 클래스의 생성자에 의해 값이 덮어씌워진다.
  3. 안전 점검 3
    Convenience 생성자는 프로퍼티에 값을 할당하기 전에 다른 생성자를 실행해야 한다. 그렇지 않으면 자체 클래스의 생성자에 의해 값이 덮어씌워진다.
  4. 안전 점검 4
    생성자는 초기화의 첫 번째 단계가 완료될 때까지 인스턴스 메서드, 인스턴스 프로퍼티, self를 사용할 수 없다.

 

클래스의 인스턴스는 첫 번째 단계가 끝날 때까지는 유효한 개체가 아니다. 즉 첫 번째 단계를 완료해야지 프로퍼티에 접근하거나 메서드를 호출할 수 있다. 위의 네 가지 안전 점검에 의해 2 단계 초기화가 수행되는 방식은 다음과 같다.

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

위의 그림은 1 단계에서 서브 클래스 및 슈퍼 클래스의 생성자를 찾는 방법이다.

 

위의 그림에서 초기화는 서브 클래스의 convenience 생성자의 호출에서 시작된다. 아직 convenience 생성자는 프로퍼티를 수정할 수 없으므로 동일한 클래스의 designated 생성자를 실행시킨다. Designated 생성자는 안전 점검 1에 따라 서브 클래스의 프로퍼티에 값이 있는지 확인한 뒤 수퍼 클래스의 designated 생성자를 호출한다. 수퍼 클래스의 designated 생성자는 수퍼 클래스의 프로퍼티에 값이 있는지 확인하고 더 이상의 수퍼 클래스가 없기 때문에 메모리는 완전히 초기화 된 것으로 간주되고 1단계가 종료된다.

이젠 초기화의 2단계를 살펴보자. 이제 슈퍼 클래스의 designated 생성자는 인스턴스를 커스터마이징 할 수 있다. (물론 꼭 해야하는 건 아니다.) 수퍼 클래스의 designated 생성자가 완료되면 서브 클래스의 designated 생성자가 커스터 마이징을 수행하고 그 후엔 convenience 생성자가 추가적인 커스터마이징을 수행 할 수 있게 된다.

 

Initializer Inheritance and Overriding

Objective-C의 서브 클래스와는 달리 Swift 서브 클래스는 기본적으로 수퍼 클래스의 생성자를 상속하지 않는다. 이러한 방식은 서브 클래스에서 정의된 복잡한 생성자가 수퍼 클래스의 생성자에 의해 실행되지 않는 오류를 방지해준다.

 

슈퍼 클래스의 생성자를 커스텀 서브 클래스의 생성자로 사용하고 싶다면 해당 생성자를 직접 구현하면 된다. 수퍼 클래스의 designated 생성자와 같은 생성자를 서브 클래스에서 작성하면 해당 생성자를 대체할 수 있는데 이럴 땐 override 키워드를 서브 클래스의 생성자 앞에 써줘야한다. 만약 default 생성자를 오버라이딩 해도 override는 붙여줘야한다. 오버라이드된 프로퍼티, 메서드, 서브스크립트와 마찬가지로 override라는 키워드는 Swift에게 수퍼 클래스에 이러한 생성자가 있는지 확인하고 생성자의 매개변수가 잘 지정되었는지 확인하게 해 준다.

 

슈퍼 클래스의 convenience 생성자와 같은 이름의 생성자를 만들고 싶은 경우엔 조금 다르다. 슈퍼 클래스의 convenience 생성자는 서브 클래스에서 직접 호출할 수 없다. 이는 클래스 타입에 따른 생성자 delegation 규칙에 때문이다. 그러므로 서브 클래스는 슈퍼클래스의 convenience 생성자에 대해서는 오버 라이딩을 제공하지 않고 따라서 같은 이름의 생성자를 만들더라도 override 키워드를 써주지 않아도 된다.

 

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

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

위의 코드는 Vehicle 클래스를 정의하고 numberOfWheels라는 저장 프로퍼티를 선언했다. 이는 description이라는 계산 프로퍼티에서 문자열을 만드는 데 사용된다. 여기서 Vehicle 클래스는 저장 프로퍼티에 default 값을 할당했고 따로 생성자를 작성하지 않았다. 결과적으론 이는 default 생성자를 사용하게 되며 이러한 default 생성자는 클래스에서 항상 designated 생성자이다.

 

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

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

이번에는 Vehicle 클래스를 상속받는 Bicycle 클래스를 정의해보자. Bicycle 클래스에는 designated 생성자인 init()을 정의해주었다. 이 생성자는 슈퍼 클래스인 Vehicle에도 있으므로 override 키워드를 앞에 써줘서 서브 클래스에서 오버라이드 했다는 것을 명시해야 한다. 그럼 이제 Bicycle의 init()을 살펴보자. 이 생성자는 super.init(), 즉 Vehicle의 생성자를 호출하고 있다. Vehicle의 생성자는 numberOfWheels 프로퍼티의 값을 0으로 초기화하는 생성자였지만 Bicycle 클래스의 init()에서 다시 이를 2로 바꿔주게 된다. 만약 이러한 동작에서 서브 클래스의 생성자가 초기화 프로세스의 2단계에서 커스텀 생성자가 없고 슈퍼 클래스의 designated 생성자에 매개변수가 없을 땐 서브 클래스의 저장 프로퍼티에 값을 할당한 후에 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)"
    }
}

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

위의 코드는 Hoverboard라는 새로운 Vehicle의 서브클래스를 정의한 것이다. Hoverboard 클래스의 생성자는 color 프로퍼티만 초기화한다. 이 생성자는 super.init()을 암시적으로 호출하여 초기화 프로세스를 완료한다. 하나 덧 붙이자면 서브 클래스는 상속된 변수 프로퍼티의 값은 수정할 수 있지만 상속된 상수 프로퍼티의 값은 수정할 수 없다.

 

Automatic Initializer Inheritance

위에서 언급했듯이 서브 클래스는 기본적으로 슈퍼클래스의 생성자를 상속하지 않는다. 하지만 특정 조건에서는 슈퍼 클래스의 생성자가 자동으로 상속된다. 이러한 기능은 특정 조건에서 슈퍼 클래스의 생성자를 상속할 수 있다는 것을 의미한다.

 

서브 클래스에 정의한 새로운 프로퍼티에 default값을 제공한다고 가정하면 다음 두 가지 규칙이 적용된다.

  • Rule 1

    • 서브 클래스가 designated 생성자를 정의하지 않으면 자동적으로 슈퍼 클래스의 designated 생성자를 모두 상속받는다.
  • Rule 2

    • 서브 클래스가 Rule 1에 따라 상속하거나 슈퍼 클래스의 모든 designated 생성자를 오버라이드 해서 구현하면 슈퍼 클래스의 convenience 생성자도 자동으로 상속받는다. 이러한 규칙은 서브 클래스에 convenience 생성자를 추가하는 경우에도 적용된다.

 

Designated and Convenience Initializers in Actions

그럼 designated 생성자, convenience 생성자, automatic 생성자의 상속이 작동하는 모습을 살펴보자.

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

우선 슈퍼 클래스로 Food라는 클래스를 정의했다. 이는 식품의 이름을 캡슐화하는 클래스이고 생성자는 2개가 정의되어있다.

위의 그림은 Food 클래스 생성자의 관계를 나타낸 것이다. Food 클래스에는 default memberwise 생성자가 없기 때문에 designated 생성자인 init(name:)를 제공한다.

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

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

init(name:) 생성자를 사용하여 Food 클래스의 새로운 인스턴스를 생성할 수 있다. Food에는 인수 없이 사용 가능한 생성자인 init()도 정의되어 있고 이를 통해서도 새로운 인스턴스를 만들 수 있다.

 

그럼 이젠 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()
let oneBacon = RecipeIngredient(name: "Bacon")
let sixEggs = RecipeIngredient(name: "Eggs", quantity: 6)

RecipeIngredient 클래스라는 Food의 서브클래스를 만들었고이는 quantity라는 저장 프로퍼티가 새로 정의되었으며 생성자도 2개 정의되어있다.

위의 그림은 RecipeIngredient 클래스의 생성자들의 관계를 나타낸 그림이다. RecipeIngredient 클래스에는 init(name: quantity:)라는 designated 생성자가 있고 이 생성자에 의해 새로 정의된 프로퍼티에 값을 할당한다. 그리고 super.init(name:)을 호출하게 되며 2단계 초기화의 안전점검 1을 만족하게 된다. RecipeIngredient 클래스는 convenience 생성자인 init(name:String)로 정의되어있는데 이를 통해 인스턴스를 더 쉽게 만들 수 있게 된다. 하지만 이 생성자는 슈퍼 클래스인 Food에서도 동일한 매개변수를 갖는 생성자가 정의되어 있기 때문에 override를 앞에 써줘야 한다.

 

RecipeIngredient는 convenience 생성자로 init(name:)을 제공하지만 슈퍼 클래스의 designated 생성자를 모두 제공했으므로 Food 클래스의 convenience 생성자도 모두 상속받게 된다. 즉 Food의 init() 생성자를 상속받게 되는 것이다.

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 ✘

이번엔 RecipeIngredient 클래스를 상속받는 ShoppingListItem 클래스를 정의해보았다. 이 클래스에는 저장 프로퍼티 purchased와 계산 프로퍼티인 description가 정의되어있다. 모든 프로퍼티에 값이 할당되어 있기 때문에 따로 생성자를 정의하지 않아도 문제가 없는 것을 알 수 있다. ShoppingListItem 클래스는 모든 프로퍼티에 값을 제공하고 생성자 자체를 정의하지 않았기 때문에 슈퍼클래스에서 정의된 모든 생성자를 상속받는다. 따라서 위의 코드처럼 ShoppingListItem의 인스턴스들을 만들 수 있다.

위의 그림은 지금까지 정의한 세 클래스의 생성자 관계를 보여준다.


Failable Initializers

클래스, 구조체, 열거형의 생성자가 실패할 수 있을 경우에 Failable 생성자(실패 가능한 생성자)는 유용하게 사용될 수 있다. 여기서 실패는 유효하지 않은 값을 프로퍼티에 할당하거나 필수 외부 자원의 부재, 초기화 성공을 방해하는 기타 조건에 의해 방생할 수 있다. 이러한 조건들이 발생시 대처하기 위해서는 하나 이상의 생성자에 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"

위의 코드와 같이 숫자 타입 간 변환에서 값을 정확하게 유지하려면 init(exactly:) 생성자를 사용하면 된다. 만약 변환 후 값을 유지할 수 없다면 생성자가 실패하게 된다. 즉 위의 코드에서는 12345.0를 Int형으로 변환해도 값이 유지되기 때문에 이땐 값이 잘 초기화되지만 3.14159를 Int로 변환하면 3인데 이는 값이 유지되지 않았으므로 초기화가 실패하게 되는 것이다.

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"

위의 코드에서는 Animal 구조체를 정의했는데 여기엔 init?(species:)이라는 실패 가능한 생성자가 정의되어있다. 이 생성자는 만약 빈 문자열이 매개변수로 들어오면 초기화를 실패하게 되도록 정의되었다. 실제로 인스턴스를 만들어보면 빈 문자열을 생성자의 매개변수로 입력하게 되면 초기화에 실패하게 된다.

 

Failable Initializers for Enumerations

실패 가능한 생성자는 열거형에도 사용할 수 있는데 이를 사용하여 적절한 열거 케이스를 선택할 수 있다. 만약 제공된 매개변수가 적절한 열거 케이스와 일치하지 않게 되면 생성자 실행이 실패하게 된다.

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

위의 코드는 TemperatureUnit이라는 구조체를 정의했고 세 가지 가능한 케이스를 정의했다. 만약 이러한 케이스에 맞지 않는 값을 매개변수로 받게 되면 실패 가능하도록 생성자를 정의한 것을 볼 수 있다. 실제 인스턴스를 생성해보면 잘 작동하는 것을 볼 수 있다.

Failable Initializers for Enumerations with Raw Values

Raq Values(원시 값)이 있는 열거형은 자동적으로 실패 가능한 생성자인 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."

위의 코드는 원시 값을 갖는 TemperatureUnit 열거형을 정의한 것이다. 따로 생성자를 정의하지 않았지만 자동적으로 init?(rawValue:)가 생성되어 사용할 수 있는 것을 볼 수 있다.

 

Propagation of Initialization Failure

클래스, 구조체, 열거형의 실패 가능한 생성자는 같은 클래스, 구조체, 열거형의 실패가능한 생성자에 위임할 수 있다. 비슷하게 서브 클래스의 실패가능한 생성자는 슈퍼 클래스의 실패가능한 생성자를 위임할 수 있다. 이러한 경우에 초기화가 실패하게 되면 전체 초기화 프로세스가 실패하고 더이상 코드가 실행되지 않는다.

 

실패 가능한 생성자는 실패 할 수 없는 생성자를 위임할 수도 있다. 이런 경우엔 다음과 같은 방법을 사용하면 된다.

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"

위의 코드는 Product 클래스와 이를 상속받는 CartItem 클래스를 정의한 코드이다. 두 클래스에는 모두 실패가능한 생성자가 정의되어있다. CartItem 클래스의 생성자는 매개변수로 받는 quantity의 값이 1보다 작다면 초기화에 실패하게 되어 더 이상 코드가 실행되지 않는다. 마찬가지로 Product 클래스의 생성자도 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"

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

다른 생성자들과 마찬가지로 서브클래스에서 슈퍼 클래스의 실패 가능한 생성자를 오버라이드 할 수 있다. 또한 서브 클래스의 실패 불가능한 생성자를 슈퍼 클래스의 실패가능한 생성자로 오버라이드 할 수도 있다. 이런 경우에 슈퍼 클래스의 생성자를 위임하는 유일한 방법은 실패가능한 슈퍼클래스의 생성자의 결과를 강제 언래핑하는 것이다. 또한 실패할 수 없는 생성자는 실패가능한 생성자를 오버라이드 할 수 있지만 그 반대는 불가능하다.

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 클래스를 정의했다. 여기서 init?(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
        }
    }
}

위의 코드는 아까 만든 Document 클래스를 상속받는 AutomaticallyNamedDocument 클래스를 정의한 것이다. AutomaticallyNamedDocument는 Document 클래스의 실패 가능한 생성자인 init?(name:)을 실패 불가능한 생성자로 오버라이드 했다.

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

위의 코드는 Document 클래스를 상속받는 UntitledDocument 클래스를 정의한 것이다. 여기서는 Document 클래스의 init() 생성자를 오버라이드 하였는데 이때 강제 언래핑을 사용하여 구현의 일부로 슈퍼 클래스의 실패 가능한 생성자를 호출할 수 있다. 이러한 경우 실패하지 않도록 매개변수를 설정해줘야 한다.

 

The init! Failable Initializer

일반적으론 실패 가능한 생성자를 init? 키워드로 만들지만 옵셔널 타입이 아닌 인스턴스를 만드는 실패가능한 생성자를 만들고 싶다면 init!을 사용하면 된다. init! 생성자를 위임할 땐 init! 생성자로 인해 실패할 수 있기 때문에 주의해야 한다.


Required Initializers

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

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

위의 코드처럼 생성자 이름 앞에 required를 써주면 이 클래스의 모든 서브 클래스는 이 생성자를 실행해야만 한다. 이를 필수 생성자라고 하며 이 생성자를 오버라이드 할 땐 override 키워드를 사용하지 않아도 된다.


Setting a Default Property Value with a Closure or Function

저장 프로퍼티의 default 값에 적절한 설정이 필요한 경우 클로저 또는 전역 함수를 사용하여 해당 프로퍼티에 대한 default값을 제공할 수 있다. 프로퍼티가 속한 인스턴스가 초기화될 때마다 클로저 또는 함수가 호출되고 반환 값이 속성의 default값으로 할당된다.

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

위의 코드는 프로퍼티에 default값을 제공하기 위해 클로저를 사용하는 방법이다. 이때 클로저의 {} 끝에 빈 괄호 쌍 ()이 온다. 이는 Swift가 클로저를 즉시 실행하도록 하며 이 괄호를 생략하면 클로저의 반환 값이 아닌 클로저 자체를 프로퍼티에 할당하려고 한다. 이렇게 클로저를 사용하여 프로퍼티를 초기화하는 경우 나머지 인스턴스는 클로저가 실행되는 시점에서는 아직 초기화되지 않았다. 이는 해당 프로퍼티에 default값이 있더라도 클로저 내에서 다른 프로퍼티의 값에 접근할 순 없다는 것을 말한다. 또한 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"

위의 코드는 체스 게임용 보드를 모델링하는 Chessboard 구조체를 정의한 것이다. 이 게임 보드를 표현하기 위해 Chessboard 구조체는 64개의 Bool값의 Array인 boardColors라는 프로퍼티를 정의했다. true는 검은색을 false는 흰색을 나타낸다.

위의 그림은 Chessboard 구조체로 만든 인스턴스를 그림으로 나타낸 것이다. 새로운 인스턴스가 생성될 때마다 클로저가 실행되고 boardColors의 default값이 계산되어 반환된다.

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함