초기화(Initialization)
은 클래스, 구조체, 열거형의 인스턴스를 사용하기 위해 준비하는 과정이다. 각각의 저장 프로퍼티에 초기 값을 설정하고, 새로운 인스턴스의 사용이 준비되기 전에 필요한 다른 세팅이나 초기화 작업을 실행한다.
이니셜라이저(initializer)
를 정의해서 초기화 과정을 구현한다. 이는 특정 타입의 새로운 인스턴스를 생성하기 위해 호출할 수 있는 특별한 메소드와 같다. Objective-C의 이니셜라이저와 달리 Swift의 이니셜라이저는 값을 반환하지 않는다. 이니셜라이저의 가장 큰 역할은 타입의 새로운 인스턴스가 처음으로 사용되기 전에 정확히 초기화됐는지를 보장하는 것이다.
클래스 타입의 인스턴스는 디이니셜라이저(deinitializer)
를 구현할 수도 있다. 이는 클래스의 인스턴스가 할당을 해제하기 전에 청소를 하는 것이다.
클래스와 구조체는 인스턴스가 생성되는 시점에 반드시 모든 저장 프로퍼티에 적당한 초기값을 설정해야 한다. 저장 프로퍼티는 미결정된 상태로 남겨질 수 없다.
이니셜라이저를 통해 저장 프로퍼티에 값을 설정하거나 프로퍼티 정의의 일부분으로써 프로퍼티 기본 값을 할당할 수 있다.
저장 프로퍼티에 기본 값을 할당하거나 이니셜라이저로 초기 값을 설정할 때, 프로퍼티의 값은 프로퍼티 옵저저버를 호출하지 않고 직접적으로 설정된다.
이니셜라이저(initializer)
는 특정 타입의 새로운 인스턴스를 생성한다. 가장 간단한 형태의 이니셜라이저는 init
키워드를 매개 변수가 없는 인스턴스 메소드처럼 작성하는 것이다.
init() {
// perform some initialization here
}
아래 예시는 화씨 온도를 저장하기 위한 Fahrenheit
구조체를 정의한다. Fahrenheit
구조체는 하나의 저장 프로퍼티인 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"
구조체는 init
키워드로 매개 변수가 없는 하나의 구조체를 정의한다. 이 이니셜라이저 안에서 temperature
를 32.0으로 초기화 한다.
이니셜라이저에서 저장 프로퍼티에 초기 값을 설정하는 대신 프로퍼티 선언에서 기본 값을 할당할 수 있다. 프로퍼티가 정의될 때 초기 값을 기본 프로퍼티 값으로써 할당한다.
만약 프로퍼티가 항상 같은 초기 값을 가진다면, 기본 값을 제공하는 것이 이니셜라이저를 사용하는 것보다 낫다. 결과는 동일하지만, 기본 값은 선언보다 프로퍼티의 초기화에 더 밀접하게 연결되어 있기 때문이다. 이는 더 짧고 명확하며, 기본 값으로부터 프로퍼티의 타입을 추론할 수 있게 해 준다. 기본 값은 또한 기본 이니셜라이저와 이니셜라이저 상속의 이점을 더 쉽게 가져 갈 수 있게 만든다.
Fahrenheit
구조체를 기본 값을 사용하여 더 단순하게 만들 수 있다.
struct Fahrenheit {
var temperature = 32.0
}
입력 매개 변수와 옵셔널 프로퍼티 타입을 사용하거나, 초기화 중에 상수 프로퍼티를 할당하여 초기화 과정을 커스터마이징 할 수 있다.
초기화 정의의 일부분으로써 초기화 과정을 커스터마이징 하는 값의 이름과 타입을 정의하기 위해 초기화 매개 변수(initialization parameter)
를 제공할 수 있다. 초기화 매개 변수는 함수/메소드 매개 변수 문법과 같은 기능을 가진다.
아래의 예시는 섭씨 온도를 저장하는 Celsius
구조체를 정의한다. Celsius
구조체는 두 개의 커스텀 이니셜라이저를 구현한다. init(fromFahrenheit:)
와 init(fromKelvin:)
이 그것이다.
struct Celsius {
var temperatureInCelsius: Double
init(fromFahrenheit fahrenheit: Double) {
temperatureInCelsius = (fahrenheit - 32.0) / 1.8
}
init(fromKelvin kelvin: Double) {
temperatureInCelsius = kelvin - 273.15
}
}
let boilingPointOfWater = Celsius(fromFahrenheit: 212.0)
// boilingPointOfWater.temperatureInCelsius is 100.0
let freezingPointOfWater = Celsius(fromKelvin: 273.15)
// freezingPointOfWater.temperatureInCelsius is 0.0
첫 번째 이니셜라이저는 fromFahrenheit
인자 라벨이 붙은 매개 변수를 갖는다. 두 번째 이니셜라이저는 fromKelvin
인자 라벨을 갖는 매개 변수를 포함한다. 두 이니셜라이저는 하나의 인자를 일치하는 Celsius
값으로 변환하고, temperatureInCelsius
프로퍼티에 값을 저장한다.
함수와 메소드 매개 변수처럼 초기화 매개 변수도 이니셜라이저의 바디에서 사용하는 매개 변수와 이니셜라이저를 호출할 때 사용하는 인자 라벨을 가질 수 있다.
하지만 이니셜라이저는 함수나 메소드가 그러는 것처럼 함수 이름을 식별하지는 않는다. 이니셜라이저 매개 변수의 타입과 이름이 어떤 이니셜라이저가 호출되는지를 식별할 때 중요한 역할을 한다. 이 때문에 Swift는 인자 라벨을 붙이지 않은 모든 매개 변수에 자동으로 인자 라벨을 제공한다.
아래 예시는 세 상수 프로퍼티를 갖고 있는 Color
구조체를 정의한다. 각 프로퍼티는 0.0에서 1.0 사이의 값을 저장한다.
Color
프로퍼티는 Double
타입의 매개 변수 세 개(red, green, blue)가 있는 이니셜라이저와 하나의 매개 변수(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)
인자 라벨 없이 이니셜라이저를 호출할 수는 없다. 인자 라벨은 반드시 이니셜라이저에서 사용되어야 한다. 생략할 경우 컴파일 시간에 에러가 발생한다.
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
값으로 초기화된다.
아래 예제는 옵셔널 프로퍼티 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."
초기화할 때 상수 프로퍼티에 값을 할당할 수도 있다. 한번 상수 프로퍼티에 값을 할당하면, 더 이상 수정할 수 없다.
클래스 인스턴스에서 상수 프로퍼티는 그것을 소개하는 클래스가 초기화 중일 때만 수정할 수 있다. 자식 클래스에서는 수정할 수 없다.
위의 SurveyQuestion
을 상수 프로퍼티를 사용하도록 변경한다. text
프로퍼티가 상수임에도 불구하고, 클래스의 이니셜라이저에서 수정할 수 있다.
class SurveyQuestion {
let text: String
var response: String?
init(text: String) {
self.text = text
}
func ask() {
print(text)
}
}
let beetsQuestion = SurveyQuestion(text: "How about beets?")
beetsQuestion.ask()
// Prints "How about beets?"
beetsQuestion.response = "I also like beets. (But not with cheese.)"
구조체나 클래스의 모든 프로퍼티가 기본 값을 갖고 있는 상황에서 이니셜라이저를 정의하지 않는다면, Swift는 기본 이니셜라이저(default initializer)
를 제공한다. 이 이니셜라이저는 모든 프로퍼티를 기본 값으로 초기화한다.
이 예시는 쇼핑 리스트에 있는 물건의 이름, 수량, 구매 상태를 캡슐화 하는 ShoppingListItem
클래스를 정의한다.
class ShoppingListItem {
var name: String?
var quantity = 1
var purchased = false
}
var item = ShoppingListItem()
모든 프로퍼티가 기본 값을 갖고 있으면서 부모 클래스가 없는 기반 클래스이기 때문에, ShoppingListItem
는 자동적으로 기본 이니셜라이저를 얻게 된다. name
프로퍼티는 옵셔널 문자열 프로퍼티여서 기본 값을 작성하지 않는 경우 nil
이 된다. ShoppingListItem()
과 같이 새로운 인스턴스를 생성할 때 기본 이니셜라이저가 사용된다.
만약 구조체가 다른 커스텀 이니셜라이저를 정의하지 않는다면, 멤버 이니셜라이저(memberwise initializer)
를 자동으로 얻는다. 기본 이니셜라이저와는 달리 구조체는 저장 프로퍼티가 기본 값을 갖지 않아도 멤버 이니셜라이저를 획득할 수 있다.
멤버 이니셜라이저는 새로운 구조체 인스턴스의 멤버 프로퍼티를 초기화하는 방법을 축약한 것이다. 새로운 인스턴스의 프로퍼티가 갖는 초기 값은 이름에 의해 멤버 이니셜라이저에 넣어질 수 있다.
아래 예시는 width
와 height
두 프로퍼티를 갖는 Size
구조체를 정의한다. 두 프로퍼티는 기본 값 0.0을 할당 받는다. Size
프로퍼티는 자동적으로 멤버 이니셜라이저인 init(width:height:)
를 얻게 된다.
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)
이라 알려진 이 과정은 다수의 이니셜라이저 간에 코드의 중복을 피한다.
이니셜라이저 위임이 작동하는 방식에 대한 규칙과 어떤 위임 형태가 혀용되는지는 값 타입과 클래스 타입이 다르다. 값 타입(구조체와 열거형)은 상속을 지원하지 않고, 따라서 이니셜라이저 위임 과정은 상대적으로 간단하다. 자기 자신을 제공하는 다른 이니셜라이저에게만 위임할 수 있다. 반면 클래스는 다른 클래스로부터 상속을 받을 수 있다. 이는 클래스는 추가적인 책임을 갖고 있다는 것을 뜻한다. 상속한 모든 저장 프로퍼티가 초기화 과정에서 적당한 값을 할당받았는지 보장해야 한다.
값 타입은 커스텀 이니셜라이저를 작성할 때 self.init
을 사용해 같은 값 타입으로부터의 다른 이니셜라이저를 참조할 수 있다. 이니셜라이저 안에서만 self.init
을 호출할 수 있다.
만약 값 타입을 위한 커스텀 이니셜라이저를 정의한다면, 그 타입의 기본 이니셜라이저(또는 멤버 이니셜라이저)에 더 이상 접근할 수 없다. 이러한 제약은 더 복잡한 이니셜라이저에 의해 제공된 필수적인 추가 셋업이 자동으로 생성된 이니셜라이저를 사용하는 무언가에 의해 순환참조되는 것을 막는다.
만약 커스텀 값 타입을 기본 이니셜라이저, 멤버 이니셜라이저, 커스텀 이니셜라이저와 함께 초기화하고 싶다면, 값 타입의 원래 구현이 아닌 확장(extension)에서 커스텀 이니셜라이저를 작성하면 된다.
다음 예시는 기하학적 사각형을 표현하는 Rect
구조체를 정의한다. 이 예시는 Size
와 Point
라는 두 가지 구조체를 필요로 한다.
struct Size {
var width = 0.0, height = 0.0
}
struct Point {
var x = 0.0, y = 0.0
}
Rect
구조체를 세 가지 방법으로 초기화할 수 있다.
origin
과 size
프로퍼티 값을 사용.origin
과 size
를 제공.center
포인트와 size
를 제공이러한 이니셜라이저 옵션들은 세 커스텀 이니셜라이저에 의해 표현된다.
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()
은 기본 이니셜라이저와 같은 기능을 한다.
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)
세 번째 이니셜라이저인 init(center:size:)
은 좀 더 복잡하다. 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
의 새로운 값을 적당한 프로퍼티에 직접 할당할 수도 있다. 하지만 기존 이니셜라이저를 사용하는 게 더 편하고 명확히 의도를 드러낼 수 있다.
모든 클래스의 저장 프로퍼티는 반드시 초기화 과정에서 초기 값을 할당 받아야 한다. 부모 클래스에서 상속 받은 프로퍼티도 마찬가지다.
Swift는 클래스 타입을 위한 두 종류의 이니셜라이저를 정의한다. 모든 저장 프로퍼티가 초기 값을 받는 걸 보장하는 데 도움을 준다.
지정 이니셜라이저(designed initializer)
는 클래스의 주 이니셜라이저이다. 지정 이니셜라이저는 클래스의 모든 프로퍼티를 완전히 초기화 한다. 그리고 부모 클래스의 이니셜라이저를 호출하여 부모 클래스 연쇄를 통한 초기화 과정을 계속 진행한다.
클래스는 최소한의 지정 이니셜라이저를 갖도록 의도된다. 일반적으로 하나의 지정 이니셜라이저를 갖는다. 지정 이니셜라이저는 초기화가 발생하고 부모 클래스 체인을 계속하는 "깔대기" 포인트와 같다.
모든 클래스는 적어도 하나의 지정 이니셜라이저를 가져야 한다. 부모 클래스로부터 하나 이상의 지정 이니셜라이저를 상속받아야 하는 경우도 있다.
편리한 이니셜라이저(convenience initializer)
는 클래스의 이니셜라이저를 지원하는 부 이니셜라이저다. 같은 클래스로부터 지정 이니셜라이저를 호출하기 위해 편리한 이니셜라이저를 정의할 수 있다. 편리한 이니셜라이저로 지정 이니셜라이저의 매개변수에 기본 값을 설정한다.
만약 클래스에서 필요하지 않다면 편리한 이니셜라이저를 만들 필요가 없다. 공통적인 초기화 패턴에 대한 축약이 시간을 절약하거나 클래스 초기화의 의도를 더 명확하게 만들 때마다 편리한 이니셜라이저를 만들면 된다.
지정 이니셜라이저는 값 타입의 이니셜라이저와 같은 방식으로 쓰여진다.
init(parameters) {
statements
}
편리한 이니셜라이저는 같은 스타일이지만, convenience
키워드를 init
앞에 붙인다.
convenience init(parameters) {
statements
}
지정 이니셜라이저와 편리한 이니셜라이저의 관계를 간단히 하기 위해, Swift는 이니셜라이저 간 위임 호출이 일어날 때 다음 세 가지 규칙을 적용한다.
간단히 말하자면 이렇다.
이미지로 표현하면 다음과 같다.
이 규칙은 클래스의 사용자가 각 클래스의 인스턴스를 만드는 방법에는 아무 영향을 끼치지 않는다. 클래스 이니셜라이저의 구현을 어떻게 작성할 것인지에만 영향을 미친다.
Swift에서 클래스 초기화는 2단계로 진행된다. 첫번째 단계에서는 각 저장 프로퍼티는 클래스에서 정한 초기값으로 초기화 된다. 모든 저장 프로퍼티의 상태가 결정되면 두번째 단계가 시작된다. 두번째 단계에서는 새로운 인스턴스의 사용이 준비됐다고 알려주기 전에 저장 프로퍼티를 커스터마이징하는 단계이다.
2단계 초기화의 사용은 클래스 상속에서 각 클래스에 완전한 융통성을 주면서도 초기화를 안전하게 만들어 준다. 2단계 초기화는 초기화되기 전에 값에 접근되는 것을 방지한다. 예상치 못한 다른 이니셜라이저에 의해 다른 값이 프로퍼티 값에 설정되는 것을 막기도 한다.
Swift의 2단계 초기화는 Objective-C에서의 초기화와 유사하다. 주된 차이점은 첫번째 단계다. Objective-C에서는 모든 프로퍼티에
0
혹은nil
값을 할당한다. Swift의 초기화는 좀 더 유연해서 커스텀한 초기 값을 할당 할 수 있다. 그리고0
과nil
이 잘못된 초기값으로 지정된 경우 대처할 수 있다.
Swift의 컴파일러는 2단계 초기화가 에러없이 끝나는 것을 보장하기 위해 4단계 안전 확인(safety-check)을 한다.
지정 이니셜라이저는 클래스의 모든 프로퍼티가 부모 클래스의 이니셜라이저에 위임하기 전에 초기화됐음을 보장해야 한다. 객체의 메모리는 모든 저장 프로퍼티의 초기 상태가 확인돼야만 완전히 초기화된 것으로 간주한다. 이러한 규칙을 만족시키기 위해 지정 이니셜라이저 반드시 그것의 모든 다른 이니셜라이저로 넘기기 전에 프로퍼티가 초기화됐는지 확인해야 한다.
지정 이니셜라이저는 반드시 상속한 프로퍼티에 값을 할당하기 전에 부모 클래스의 이니셜라이저로 위임해야 한다. 그렇지 않으면 지정 이니셜라이저가 할당한 새로운 값은 부모 클래스에 의해 덮어 쓰여진다.
편리한 이니셜라이저는 어떤 프로퍼티에 값을 할당하기 전에 반드시 다른 이니셜라이저에 위임해야 한다. 그렇지 않으면 편리한 이니셜라이저가 할당한 새로운 값은 클래스의 지정 이니셜라이저에 의해 덮어 쓰여진다.
이니셜라이저는 초기화의 1단계가 끝나기 전까지 다른 인스턴스 메소드를 호출하거나, 인스턴스 프로퍼티의 값을 읽거나, 값으로써 self
를 참조할 수 없다.
클래스 인스턴스는 1단계가 끝나기 전까지는 완전히 유효하지 않다. 1단계 종료 시점부터 프로퍼티에 접근하거나, 메소드를 호출할 수 있고, 클래스 인스턴스가 유효해진다.
4단계 안전 확인에 기반하여 2단계 초기화는 다음과 같이 작동한다.
self
에 접근 가능하며, 그것의 프로퍼티를 수정하고 인스턴스 메소드를 호출할 수 있다.self
를 사용할 수 있는 옵션을 갖게 된다.2단계 초기화를 이미지로 나타내면 다음과 같다.
Objective-C의 자식 클래스와는 달리, Swift의 자식 클래스는 부모 클래스의 이니셜라이저를 기본 값으로 상속하지 않는다. Swift의 접근은 부모 클래스의 간단한 이니셜라이저가 더 복잡한 자식 클래스에 상속되어서, 자식 클래스의 새로운 인스턴스가 완전히 초기화되지 않은 상태로 생성되는 것을 막는다.
부모 클래스의 이니셜라이저는 안전하고 적절한 특정 상황에서는 상속된다.
만약 부모 클래스와 동일한 이니셜라이저를 자식 클래스에서 만들고자 한다면, 자식 클래스에서 이런 이니셜라이저의 커스텀 구현을 제공할 수 있다.
부모 클래스의 지정 이니셜라이저와 매치되는 자식 클래스 이니셜라이저를 만들 때, 지정 이니셜라이저를 오버라이드 할 수 있다. 따라서 자식 클래스의 이니셜라이저를 정의할 때 override
키워드를 붙여야 한다. 자동으로 제공된 기본 이니셜라이저를 오버라이딩 할 때도 그래야 한다.
오버라이드된 프로퍼티, 메소드, 서브스크립트처럼 override
키워드의 존재는 Swift에게 부모 클래스가 매치되는 지정 니니셜라이저를 갖고 있는지, 오버라이딩 하는 이니셜라이저의 매개 변수는 유효한지 확인하게 한다.
부모 클래스의 지정 이니셜라이저를 오버라이딩 할 때는 자식 클래스에서 구현하는 이니셜라이저가 편리한 이니셜라이저이더라도 항상
override
키워드를 작성해야 한다.
거꾸로, 만약 부모 클래스의 편리한 이니셜라이저와 매치되는 자식 클래스의 이니셜라이저를 작성한다면, 부모 클래스의 편리한 이니셜라이저는 위에서 언급한 이니셜라이저 위임 규칙 때문에 자식 클래스에 의해 절대 직접적으로 호출되지 않는다. 그러므로, 자식 클래스는 부모 클래스 이니셜라이저를 오버라이딩 할 수 없다. 결과적으로, 부모 클래스의 편리한 이니셜라이저와 매치되는 이니셜라이저를 구현할 때는 override
키워드를 쓰면 안 된다.
아래 예시는 Vehicle
기반 클래스를 정의한다. 이 기반 클래스는 기본 값이 0인 numberOfWheels
저장 프로퍼티를 선언한다. numberOfWheels
프로퍼티는 계산 프로피티인 description
에서 차량의 특성을 묘사하기 위해 사용된다.
class Vehicle {
var numberOfWheels = 0
var description: String {
return "\(numberOfWheels) wheel(s)"
}
}
Vehicle
클래스는 저장 프로퍼티에만 기본 값을 제공하고, 다른 커스텀 이니셜라이저가 없다. 그 결과 기본 이니셜라이저가 자동으로 생성된다. 기본 이니셜라이저는 항상 지정 이니셜라이저이고, 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
버전임을 표시하기 위해 override
키워드를 붙인다.
Bicycle
의 init()
이니셜라이저는 super.init()
을 통해 Bicycle
의 부모 클래스로부터 기본 클래스를 호출한다. 이는 상속한 numberOfWheels
프로퍼티가 Vehicle
에서 초기화되었음을 보장한다.
Bicycle
의 인스턴스를 만든다면 상속된 계산 프로퍼티인 description
을 호출할 수 있다.
let bicycle = Bicycle()
print("Bicycle: \(bicycle.description)")
// Bicycle: 2 wheel(s)
만약 자식 클래스의 이니셜라이저가 2번째 초기화 단계에서 아무런 커스터마이징도 하지 않고, 부모 클래스가 인자 없는 이니셜라이저를 갖고 있다면, super.init()
을 생략할 수 있다.
이 예시에선 Vehicle
의 자식 클래스인 Hoverboard
를 정의한다. 이니셜라이저에서 Hoverboard
클래스는 color
프로퍼티에만 값을 할당한다. 명시적으로 super.init()
을 호출하는 대신 이 이니셜라이저는 암시적 호출에 의존한다.
class Hoverboard: Vehicle {
var color: String
init(color: String) {
self.color = color
// super.init() implicitly called here
}
override var description: String {
return "\(super.description) in a beautiful \(color)"
}
}
Hoverboard
의 인스턴스는 Vehicle
이니셜라이저에서 지원되는 numberOfWheels
기본 값을 사용한다.
let hoverboard = Hoverboard(color: "silver")
print("Hoverboard: \(hoverboard.description)")
// Hoverboard: 0 wheel(s) in a beautiful silver
자식 클래스는 초기화 도중 상속한 변수 프로퍼티를 수정할 수 있지만, 상수 프로퍼티를 수정할 수는 없다.
위에서 언급한 것처럼, 자식 클래스는 부모 클래스의 이니셜라이저를 기본으로 상속하지는 않는다. 하지만 특정한 조건을 충족한다면 부모 클래스의 이니셜라이저가 자동으로 상속된다. 사실 많은 상황에서 직접 이니셜라이저를 오버라이드 할 필요는 없다. 그렇게 하는 게 안전할 때만 최소한의 노력으로 부모 클래스 이니셜라이저를 상속할 수 있다.
자식 클래스에서 추가한 모든 프로퍼티에 기본 값을 제공하면 다음 두 가지 규칙이 적용된다.
만약 자식 클래스가 지정 이니셜라이저를 정의하지 않았다면, 자동으로 부모 클래스의 모든 지정 이니셜라이저를 상속한다.
만약 자식 클래스가 부모 클래스의 모든 지정 이니셜라이저를 구현한다면, 자동으로 부모 클래스의 편리한 이니셜라이저를 상속한다.
규칙 2에 따라 자식 클래스는 부모 클래스의 지정 이니셜라이저를 자식 클래스의 편리한 이니셜라이저로 구현할 수 있다.
다음 예시는 지정 이니셜라이저, 편리한 이니셜라이저, 자동으로 상속된 이니셜라이저를 보여 준다. Food
, RecipeIngredient
, ShoppingListItem
클래스의 계층과 이니셜라이저가 상호작용하는 방식을 정의한다.
Food
는 계층의 기반 클래스이다.
class Food {
var name: String
init(name: String) {
self.name = name
}
convenience init() {
self.init(name: "[Unnamed]")
}
}
클래스는 기본 멤버 이니셜라이저를 갖고 있지 않으며, Food
클래스는 하나의 인자(name
)을 갖는 지정 이니셜라이저를 제공한다.
let namedMeat = Food(name: "Bacon")
// namedMeat's name is "Bacon"
새로운 Food
인스턴스의 모든 저장 프로퍼티가 완전히 초기화되는 것을 보장하기 때문에 init(name: String)
이니셜라이저는 지정 이니셜라이저가 된다. Food
클래스는 부모 클래스가 없기 때문에 super.init()
을 호출할 필요가 없다.
Food
클래스는 인자가 없는 편리한 이니셜라이저인 init()
을 제공한다. init()
이니셜라이저는 init(name: String)
에 [Unnamed]
를 name
값으로 하여 위임한다.
let mysteryMeat = Food()
// mysteryMeat's name is "[Unnamed]"
계층의 두 번째 클래스는 Food
의 자식 클래스인 RecipeIngredient
다. 이 클래스는 quantity
프로퍼티를 선언한다. 그리고 두 개의 이니셜라이저를 정의한다.
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)
을 갖는다. 모든 프로퍼티를 채우는 데 사용된다. 이 이니셜라이저는 quantity
인자를 quantity
프로퍼티에 할당한다. 그런 뒤에 Food
클래스의 init(name: String)
에 이니셜라이저를 위임한다. 이 과정은 2단계 초기화의 안전 확인 1단계를 만족한다.
RecipeIngredient
은 편리한 이니셜라이저인 init(name: String)
도 정의한다. 이 편리한 이니셜라이저는 명시적 quantity
가 없이 생성된 모든 RecipeIngredient
의 인스턴스는 quantity
가 1이라고 가정한다. 편리한 이니셜러이저는 RecipeIngredient
의 인스턴스를 더 빠르고 편리하게 만들 수 있게 한다. 그리고 quantity
가 1인 RecipeIngredient
인스턴스를 여러 개 만들 때 코드의 중복을 피할 수 있다.
init(name: String)
편리한 이니셜라이저는 Food
의 init(name: String)
지정 이니셜라이저와 같은 매개 변수를 취한다. 이 편리한 이니셜라이저가 부모 클래스의 지정 이니셜라이저를 오버라이드 하기 때문에, override
키워드를 표시해야만 한다.
RecipeIngredient
의 init(name: String)
가 편리한 이니셜라이저이긴 하지만, RecipeIngredient
는 부모 클래스의 모든 이니셜라이저를 구현하고 있다. 따라서 RecipeIngredient
는 부모 클래스의 편리한 이니셜라이저 역시 자동으로 상속할 수 있다. 따라서 Food
의 편리한 이니셜라이저인 init()
역시 사용 가능하다.
let oneMysteryItem = RecipeIngredient()
let oneBacon = RecipeIngredient(name: "Bacon")
let sixEggs = RecipeIngredient(name: "Eggs", quantity: 6)
계층의 세 번째이자 마지막 클래스는 RecipeIngredient
의 자식 클래스인 ShoppingListItem
이다.
쇼핑 리스트의 모든 물건은 "구매되지 않은" 상태로 시작한다. ShoppingListItem
은 Boolean
프로퍼티인 purchased
를 선언한다. 프로퍼티의 기본 값은 false
다.
class ShoppingListItem: RecipeIngredient {
var purchased = false
var description: String {
var output = "\(quantity) x \(name)"
output += purchased ? " ✔" : " ✘"
return output
}
}
ShoppingListItem
은purchased
에 초기 값을 지정하기 위한 이니셜라이저를 정의하지 않는다. 쇼핑 리스트의 모든 물건은 구매하지 않은 상태로 시작하기 때문이다.
새로운 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 ✘
실패할 수 있는 이니셜라이저를 사용하는 게 유용할 때가 있다. 이 실패는 유효하지 않은 매개 변수, 요구되는 외부 자원의 부재, 또는 초기화 성공을 막는 다른 조건에 의해 발생한다.
실패할 수 있는 초기화 조건에 대처하기 위해 하나 이상의 실패 가능한 이니셜라이저를 정의한다. init
키워드 뒤에 물음표를 붙임으로써 실패 가능한 이니셜라이저를 작성한다(init?
).
실패 가능한 이니셜라이저와 일반 이니셜라이즈를 같은 매개 변수 타입과 이름을 갖도록 정의할 수는 없다.
실패 가능한 이니셜라이저는 초기화한 타입의 옵셔널 값을 생성한다. 실패 가능한 이니셜라이저 안에 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"
아래의 예시는 species
문자열 상수 프로퍼티를 갖는 Animal
구조체를 정의한다. 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")
// 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
을 확인하는 것은 다른 문제다. 위 예시에서, 빈 문자열("")은 유효한 비옵셔널 문자열이다. 하지만species
프로퍼티의 값으로는 적절하지 않다.
열거형에도 실패 가능한 이니셜라이저를 사용할 수 있다. 이니셜라이저는 매개 변수가 적절한 열거형 케이스와 매칭되지 않을 경우 실패할 수 있다.
아래 예시는 TemperatureUnit
열거형을 정의한다. 적절한 Character
열거형 케이스를 찾기 위해 실패 가능한 이니셜라이저를 사용한다.
enum TemperatureUnit {
case kelvin, celsius, fahrenheit
init?(symbol: Character) {
switch symbol {
case "K":
self = .kelvin
case "C":
self = .celsius
case "F":
self = .fahrenheit
default:
return nil
}
}
}
세 가지 가능한 상태 중 적절한 열거형 케이스를 고르고, 만약 매개 변수가 이들 중 하나랑도 맞지 않을 경우 실패를 유발하기 위해 실패 가능한 이니셜라이저를 사용할 수 있다.
let fahrenheitUnit = TemperatureUnit(symbol: "F")
if fahrenheitUnit != nil {
print("This is a defined temperature unit, so initialization succeeded.")
}
// Prints "This is a defined temperature unit, so initialization succeeded."
let unknownUnit = TemperatureUnit(symbol: "X")
if unknownUnit == nil {
print("This is not a defined temperature unit, so initialization failed.")
}
// Prints "This is not a defined temperature unit, so initialization failed."
raw 값을 사용하는 열거형은 자동으로 실패 가능한 이니셜라이저 init?(rawValue:)
를 얻는다. 이 이니셜라이저는 적절한 raw 값 타입의 rawValue
매개 변수를 가지며, 매칭되는 열거형 케이스를 찾는다. 찾지 못할 경우 실패를 발생시킨다.
TemperatureUnit
를 Character
타입의 raw 값을 사용하도록 수정할 수 있다.
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
의 자식 클래스인 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)
}
}
실패 가능한 이니셜라이저는 quantity
값이 1 이상인지 확인한다. 만약 quantity
가 유효하지 않다면 전체 초기화 과정은 즉시 실패하고 더 이상의 코드 진행은 이루어지지 않는다. 비슷하게, 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"
자식 클래스에서 실패 가능한 이니셜라이저를 오버라이드 할 수 있다. 주모 클래스의 실패 가능한 이니셜라이저를 자식 클래스의 일반 이니셜라이저로 오버라이드 할 수도 있다. 이것은 초기화를 부모클래스가 초기화 실패를 허용함에도 불구하고 실패하지 않는 자식 클래스를 정의할 수 있게 한다.
부모 클래스의 실패 가능한 이니셜라이저를 자식 클래스의 일반 이니셜라이저로 오버라이드 한다면, 부모 클래스의 이니셜라이저로 위임하는 방법은 실패 가능한 부모 클래스 이니셜라이저의 결과를 강제로 푸는(force-unwrap) 것이다.
실패 가능한 이니셜라이저를 일반 이니셜라이저로 오버라이드 할 수 있지만, 반대는 불가능하다.
아래 예시는 Document
클래스를 정의한다. 이 클래스는 비어 있지 않은 문자열 값이나 nil
이 될 수도 있는 name
프로퍼티를 갖는다. 빈 문자열은 허용하지 않는다.
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
를 정의한다. AutomaticallyNamedDocument
는 Document
의 두 지정 이니셜라이저를 오버라이드 한다. 이 오버라이드들은 AutomaticallyNamedDocument
인스턴스가 만약 이름이 없이 초기화될 경우 [Untitled]
를 name
값으로 가지며, 빈 문자열은 init(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
}
}
}
AutomaticallyNamedDocument
는 부모 클래스의 init?(name:)
을 일반 이니셜라이저인 init(name:)
으로 오버라이드 한다. AutomaticallyNamedDocument
빈 문자열이 들어왔을 때 부모 클래스와 다르게 대처함으로써 이니셜라이저는 실패할 필요가 없어졌다.
자식 클래스의 이니셜라이저를 구현하면서 부모 클래스의 실패 가능한 이니셜라이저를 호출할 때 강제로 푸는 것이 가능하다. 예를 들어 UntitledDocument
자식 클래스의 name
은 항상 [Untitled]
이 된다.
class UntitledDocument: Document {
override init() {
super.init(name: "[Untitled]")!
}
}
만약 부모 클래스의 init(name:)
이니셜라이저가 name
이 빈 문자열로 호출되면, 강제 언래핑 연산자가 런타임 에러를 발생시킨다. 그러나 문자열 상수로 호출되어 이니셜라이저가 실패하지 않으므로, 이 경우에는 런타임 오류가 발생하지 않는다.
암시적으로 풀린(implicitly unwrapped) 옵셔널 인스턴스를 생성하는 실패 가능한 이니셜라이저를 정의할 수 있다. init
키워드 뒤에 물음표 대신 느낌표를 붙인다.
init?
을 init!
으로 위임하거나, init?
을 init!
으로 오버라이드 할 수 있다. 반대의 경우도 가능하다. init
에서 init!
으로 위임할 수도 있다. init!
이니셜라이저는 초기화에 실패할 경우 assertion을 일으킨다.
required
키워드를 클래스 이니셜라이저 선언 앞에 작성하여 모든 자식 클래스는 이 이니셜라이저를 반드시 구현해야 함을 알릴 수 있다.
class SomeClass {
required init() {
// initializer implementation goes here
}
}
자식 클래스에서 요구된 이니셜라이저를 구현할 때 반드시 required
키워드를 붙여야 한다. 요구된 지정 이니셜라이저를 오버라이드 할 때는 override
를 작성하지 않는다.
class SomeSubclass: SomeClass {
required init() {
// subclass implementation of the required initializer goes here
}
}
상속된 이니셜라이저가 요구 사항을 만족시킬 수 있다면 요구된 이니셜라이저를 명시적으로 구현할 필요는 없다.
저장 프로퍼티의 기본 값이 커스터마이징이나 셋업을 요구할 경우, 프로퍼티 기본 값에 클로저나 전역 함수를 이용할 수 있다. 그 프로퍼티가 속한 타입의 새로운 인스턴스가 초기화될 때마다 클로저나 함수가 호출된다. 그리고 반환 값이 프로퍼티 기본 값으로 할당된다.
이런 종류의 클로저나 함수는 일반적으로 프로퍼티와 같은 타입의 임시 값을 반환한다. 이 임시 값은 프로퍼티 기본 값으로써 사용된다.
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
프로퍼티를 갖는다. 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]
}
}
let board = Chessboard()
print(board.squareIsBlackAt(row: 0, column: 1))
// Prints "true"
print(board.squareIsBlackAt(row: 7, column: 7))
// Prints "false"
새로운 Chessboard
인스턴스가 만들어질 때마다 클로저가 실행되고, boardColors
의 기본 값이 계산되어 반환된다.