swift initialization
Initialization
- 초기화는 클래스, 구조체 또는 열거형을 사용하기 위해 준비하는 과정.
- 각각의 저장 프로퍼티 (Property) 에 초기값을 넣어주고, 새로운 인스턴스가 사용되기 전 각종 초기작업을 할 수 있는 life cycle
- swift 의 initializer 는 objective-c 의 init 처럼 값을 반환하지는 않는다.
- initilization 의 주요한 역할은 새로운 인스턴스가 처음으로 사용되기 전 정확하게 초기화되었는지 보장하는 것
Setting initial values for stored properties
- class 나 struct 의 저장 프로퍼티는 indeterminate 상태로 남을 수 없다. 아래 코드를 보자
class SampleClass {
let name: String
}
하나의 저장 프로퍼티를 가지고 있는 SampleClass 다. 그러나 위 코드를 그대로 작성하면 컴파일 에러가 발생한다. 저장 프로퍼티인 name 은 무조건 초기값이 존재해야 한다. 왜냐하면 옵셔널 타입이 아니라서 nil 을 가질 수도 없고, 사용자가 인스턴스를 생성해서 name 에 접근했을 때 String value 를 가지고 있어야 하기 때문이다.
그러나 위 코드에서 name 은 아무런 초기값도 가지고 있지 않기 때문에 컴파일 에러가 발생한다. 즉 name 은 indeterminate 상태, 정해지지 않은 상태로 남기 때문이다.
위와 같은 저장 프로퍼티에는 다음과 같이 두 가지의 방식으로 값을 할당할 수 있다.
1. Default Property values
class SampleClass {
let name: String = "Hello World!"
}
위와 같이 저장 프로퍼티 선언과 함께 초기값을 지정할 수 있다.
2. initializers
class SampleClass {
let name: String
init() {
name = "Hello World"
}
}
Initializer 는 새로운 인스턴스를 생성하기 위해 호출된다. init 이란 키워드를 통해 정의되며, 만약 init 코드 블록이 끝날 때 까지 초기화되지 않는 저장 프로퍼티가 있다면 역시 컴파일 에러가 발생한다.
Customizing Initialization
init 함수에 파라미터를 넣어서, 초기값을 개발자들로부터 직접 입력받을 수도 있다. 아래 예제를 보도록 하자.
struct Celsius {
var temperatureInCelsius: Double
init(fromFahrenheit fahrenheit: Double) {
temperatureInCelsius = (fahrenheit - 32.0) / 1.8
}
init(fromKelvin kelvin: Double) {
temperatureInCelcius = kelvin - 273.15
}
init(_ celsius: Double) {
temperatureInCelsius = celsius
}
}
let celsius1 = Celsius(fromFahrenheit: 130)
let celsius2 = Celsius(fromKelvin: 290)
let celsius3 = Celsius(37)
위 코드에서 우리는 다음과 같은 사실을 알 수 있다.
- 여러 개의 custom initializer 를 구현할 수 있다.
- 파라미터를 받아서 저장 프로퍼티 초기화를 할 수 있다.
또한 swift 의 특징으로 개발자에게 매개변수의 이름을 fromKelvin, fromFahrenheit 으로 보여줄 수 있다. 매개변수 네이밍을 통해 좀 더 직관적인 초기화 개념을 개발자에게 제시해줄 수 있으며, 항상 네이밍에 신경 쓰도록 하자.
Optional Property Types
옵셔널 타입으로 선언된 저장 프로퍼티는 초기값을 지정해주지 않으면, 자동으로 nil이 할당된다.
class SurveyQuestion {
var text: String
var response: String?
init(text: String) {
self.text = text
}
}
값을 가질 수도 있고 안 가질 수도 있는 프로퍼티는, 우리가 옵셔널 타입으로 선언한다. 설문 조사를 생각해보자. 여러 개의 질문이 있고, 각 질문에 사용자는 대답을 할 수도 있고 안 할 수도 있다. 옵셔널 타입으로 선언된 저장 프로퍼티는 항상 초기화해야 할 의무가 없다.
Constant Property type
우리는 변할 필요가 없는 프로퍼티나 변수를 let 키워드와 함께 선언한다. let 은 불변 값 이기 때문에 항상 선언과 함께 값을 할당해야 하는데, Struct 나 Class 와 같은 타입 내부의 상수 프로퍼티는 무조건 선언과 동시에 값을 할당해 줄 필요가 없다. 그 대신 initializer 가 끝나기 전까지 값을 무조건 할당해야 한다.
사실 위의 설문조사 struct 에서, 질문 내용 자체는 변하지 않는다. 그렇다면 우리는 var text 를 let text 로 수정할 수 있다.
class SurveyQuestion {
let text: String
var response: String?
init(text: String) {
self.text = text
}
}
위의 예제를 보면 let 으로 선언된 text 프로퍼티에 굳이 초기값을 할당해주지 않더라도, initializer 에서 값을 할당해주기만 하면 아무런 문제없이 코드가 컴파일 되는 것을 확인할 수 있다.
Initializer Delegation for value types
각 initializer 는 다른 initializer 들을 호출할 수 있다. 이것을 initializer delegation 이라고 부르는데, 여러 개의 initializer 중 중복해서 나타나는 코드를 줄일 수 있게 해준다. 그러나 struct 와 같은 값 타입 (Value type) 과 class 와 같은 참조 타입 (reference type) 에 따라 initializer delegation 는 다른 역할을 수행한다.
값 타입은 상속을 지원하지 않기 때문에, 참조 타입에 비해 상대적으로 단순하게 진행된다. 왜냐하면 sturct 자체적으로 제공하는 다른 initializer 들만 사용할 수 있기 때문이다.
그러나 클래스는 다른 클래스를 상속할 수 있다. 이 말은 즉 초기화 중에 상속받는 모든 저장 프로퍼티가 제대로 초기화되었는지 보장해야 하는 책임을 가지게 되는 것이다.
값 타입의 initializer delegation
// 사각형의 넓이, 높이를 나타내는 Size 구조체
struct Size {
var width = 0.0, height = 0.0
}
// 좌표상의 위치를 나타내는 Point 구조체
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)
}
}
사각형은 좌측 상단의 좌표값을 가지고, 자신의 크기를 갖는다. 위 코드에서 Rectangle 구조체를 초기화하기 위한 3가지의 이니셜라이저를 제공한다.
하나는 기본 이니셜라이저고, 두 번째 이니셜라이저는 원점의 좌표와 크기를 받는 이니셜라이저다. 마지막으로 세 번째 이니셜라이저는 중앙의 좌표가 크기를 받는 이니셜라이저인데, 중앙 좌표와 크기를 이용해 원점의 좌표를 계산한 뒤, 구조체 내부의 다른 이니셜라이저를 호출 (self.init) 하는 것을 확인할 수 있다.
Class inheritance and initialization
클래스는 상속할 수 있다. 즉 슈퍼 클래스가 있을 수도 있다는 말이고, 슈퍼클래스로부터 상속받은 모든 저장 프로퍼티는 초기화 시 값을 할당해야 한다. swift 는 클래스 타입의 모든 저장 프로퍼티에 초기값을 할당하도록 도와주는 두 가지 이니셜라이저를 정의한다.
1. designated initializer - 지정 이니셜라이저
2. convenience initializer - 편의 이니셜라이저
지정 이니셜라이저는 클래스를 위한 주요 이니셜라이저이다. 지정 이니셜라이저는 class 의 모든 저장 프로퍼티를 초기화해야 하는 의무를 가지고 있으며, 또한 적합한 슈퍼클래스 이니셜라이저를 호출하여 초기화 과정을 부모클래스로 연쇄하기도 한다.
편의 이니셜라이저는 보조적인 역할을 하며, 편의 이니셜라이저를 선언하여 기본값을 사용해 지정 이니셜라이저를 호출할 수 있다.
편의 이니셜라이저가 존재할 필요가 없으면 굳이 선언하지 않아도 된다.
Syntax for Designated and Convenience Initializers
1. Designated initializers for class
init(parameters) {
statements
}
2. Convenience initializers for class
convenience init(parameters) {
statements
}
Initializer Delegation for class type
지정 이니셜라이저와 편의 이니셜라이저 사이의 관계를 명확하게 하기 위해, swift 는 아래와 같은 세 가지 규칙을 적용한다.
규칙 1. 지정 이니셜라이저는 직접 관련있는 슈퍼클래스로부터 지정 이니셜라이저를 호출해야 한다.
규칙 2. 편의 이니셜라이저는 같은 클래스에서 다른 이니셜라이저를 호출해야 한다.
규칙 3. 편의 이니셜라이저는 지정 이니셜라이저로 끝맺어야 한다.
위 그림은 3가지의 규칙을 그림으로 표현한 것이다.
- Super class 는 2개의 편의 이니셜라이저와, 1개의 지정 이니셜라이저를 가지고 있다.
- Subclass 는 2개의 지정 이니셜라이저와, 1개의 편의 이니셜라이저를 가지고 있다.
- Super class 에서, 하나의 편의 이니셜라이저가 다른 편의 이니셜라이저를 부르고, 해당 편의 이니셜라이저가 결국 지정 이니셜라이저를 호출한다. -> 따라서 규칙2 와 3을 만족한다.
- Subclass 에서 지정 이니셜라이저는 슈퍼클래스의 지정 이니셜라이저를 호출한다. -> 규칙 1을 만족한다.
- Subclass에서 하나의 편의 이니셜라이저는 같은 클래스 내의 지정 이니셜라이저를 호출하고, 지정 이니셜라이저는 슈퍼클래스의 지정 이니셜라이저를 호출한다. 따라서 규칙 1과 2, 그리고 3을 모두 만족한다.
위 그림은 이전 다이어그램보다 훨씬 복잡해졌지만, 하나하나 뜯어보면 결국 규칙 1과 2, 그리고 3을 모두 만족하는 다이어그램임을 확인할 수 있다.
Two-Phase Initialization
Swift 에서 클래스 초기화는 두 단계로 진행된다. 첫 단계에선 각 저장 프로퍼티에 초기값이 할당된다. 각 프로퍼티의 초기값이 모두 결정되었으면, 두 번째 단계가 시작한다. 그리고 두 번째 단계에선 클래스가 실제로 사용되기 전까지 저장 프로퍼티를 커스텀할 기회가 생긴다.
swift 컴파일러는 two-phase initialization 이 에러 없이 종료될 수 있도록 아래와 같이 4 가지 safety check 를 진행한다.
- 지정 이니셜라이저는 클래스에 새로 도입된 저장 프로퍼티들이 슈퍼클래스 이니셜라이저에 위임되기 전에 초기화되는지 확실하게 해야한다.
InheritedClass 에서 새로 선언된 newValue 가 초기화되기전 super.init(value: 0) 을 호출했다. safety check 1단계에서 컴파일 에러가 발생한다. 슈퍼클래스에 초기화를 위임하기 전에 newValue 를 초기화하면 에러를 해결할 수 있다.
2. 지정 이니셜라이저는 상속받은 프로퍼티에 값을 할당하기 전에 슈퍼 클래스 이니셜라이저로 위임해야 한다. 그렇지 않으면 지정 이니셜라이저의 새로운 값은 슈퍼클래스의 초기화로부터 덮어씌여진다.
3. 편의 이니셜라이저는 프로퍼티에 값을 할당하기 전에 다른 이니셜라이저에 위임해야 한다. 만약 그렇지 않다면 클래스의 지정 이니셜라이저에 의해 편의 이니셜라이저에서 할당한 값이 덮여씌어질 것이다.
4. 이니셜라이저는 인스턴스 메소드를 호출할 수 없으며 인스턴스 속성의 값을 읽을 수 있거나 초기화 첫 번째 단계가 완료될 때까지 값으로 self 로 참조한다.
아래 그림을 통해 위 4가지의 safety check 와 two-phase initialize 과정을 좀 더 자세히 이해해보자!
초기화 1단계
- 서브 클래스의 편의 이니셜라이저가 호출된다. 아직까지 해당 편의 이니셜라이저에서 프로퍼티를 수정할 권한은 없다. 편의 이니셜라이저는 우선 같은 클래스 내의 지정 이니셜라이저에 초기화를 위임한다.
- subclass 에서 새롭게 선언된 저장 프로퍼티들에 초기값을 할당하고, super class 의 지정 이니셜라이저를 호출한다.
- super class 의 지정 이니셜라이저는 super class 의 모든 저장 프로퍼티들이 값을 가지고 있는지 보장한다. 더 이상 슈퍼클래스가 존재하지 않으므로, 초기화 1단계가 끝이 난다.
- 초기화 1단계가 끝났으므로, 2단계에서 각 이니셜라이저는 인스턴스를 추가로 조작할 기회를 얻는다. 프로퍼티에 새로운 값을 할당하거나, 인스턴스 메소드를 호출할 수 있다.
초기화 2단계
- 이제 슈퍼클래스의 지정 이니셜라이저는 인스턴스를 추가로 조작할 기회를 얻는다.
- 그 다음 하위클래스의 지정 이니셜라이저가 인스턴스를 추가로 조작할 기회를 얻는다.
- 마지막으로 편의 이니셜라이저가 추가적으로 인스턴스를 조작할 기회를 얻는다.
References
Initialization — The Swift Programming Language (Swift 5.7)
Initialization Initialization is the process of preparing an instance of a class, structure, or enumeration for use. This process involves setting an initial value for each stored property on that instance and performing any other setup or initialization t
docs.swift.org