[Swift] initialization (1)

LEEHAKJIN-VV·2022년 5월 18일
0

Study-Swift 5.6

목록 보기
15/22

참고사이트:
English: The swift programming language
Korean: The swift programming language


Initialization (초기화)

Initalization(초기화)는 클래스, 구조체, 열거형의 인스턴스를 사용하기 위한 준비과정으로 이 과정에서 stored property(저장 프로퍼티)의 초깃값을 할당한다. Initializers(이니셜라이저)를 호출하여 초기화 과정을 수행할 수 있다. Initializers는 특정 유형의 새 인스턴스를 호출할 때 사용하는 특별한 메소드로, Swift의 initializers는 값을 반환하지 않는다. 클래스 타입의 인스턴스는 할당된 자원을 해지하기 위해 deinitialization을 사용한다.


Setting Initial Values for Stored Properties (저장프로퍼티를 위한 초기값 할당)

클래스나 구조체의 인스턴스가 만들어 질때 각각의 인스턴스의 stored properties는 적절한 값으로 초기화 되어야 한다.

NOTE
Initialize내에서 stoed property의 값에 기본값이나 특정 값을 직접 할당하면 property observer가 호출되지 않는다.

Initializers (이니셜라이저)

Initializers는 특정 타입의 새로운 인스턴스를 만들기 위해 호출된다. Initializers의 가장 간단한 형태는 init 키워드로 작성된 파라미터가 없는 인스턴스 메소드이다.

init() {
    // perform some initialization here
}

아래 예제는 1개의 stored property를 가지는 구조체 Fahrenheit를 선언하였다. temperature는 stored property로 이니셜라이져 구문에서 초기화된다.

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 (기본 프로퍼티 값)

위의 예제처럼 이니셜라이저내에서 저장 프로퍼티의 초기값을 할당할 수 있다. 이와 유사한 방법으로 프로퍼티의 선언에서 프로퍼티의 기본값을 할당할 수 있다.

NOTE
만약 프로퍼티가 항상 같은 초깃값을 가진다면 이니셜라이저에서 값을 할당하는 것 대신 기본 프로퍼티 값을 가지는게 낫다. 이 기본값은 상속시에 서브클래스에 같이 상속된다.

위의 예제를 기본 프로퍼티를 사용하여 프로퍼티를 선언함과 동시에 초깃값을 할당하였다.

struct Fahrenheit {
    var temperature = 32.0
}

Customizing Initialization (커스터마이징 초기화)

입력 파라미터 또는 옵셔널 프로퍼티 타입 혹은 초기화 중에 상수 프로퍼티에 값을 할당하여 초기화 과정을 커스터마이징 할 수 있다. 이 방법은 다음 섹션에서 다룬다.


Initialization Parameters(초기화 파라미터)

초기화 과정에서 값의 타입과 이름을 지정하기 위해 초기화 선언의 일부로 이니셜라이저 파라미터를 제공할 수 있다. 이니셜라이저 파라미터는 함수나 메소드의 파라미터와 문법과 기능이 동일하다.

다음 예제는 구조체 Celsius를 정의하였고,init(fromFahrenheit:), init(fromKelvin:)라는 2개의 custom이니셜라이저를 호출한다.

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

각각의 이니셜라이저는 인자를 일치하는 섭씨온도로 변환하고 temperatureInCelsius프로퍼티에 저장한다.


Parameter Names and Argument Labels(파라미터 이름과 인자 라벨)

함수와 메소드 파라미터처럼 초기화 파라미터는 이니셜라이저 구문에서 사용하기 위한 파라미터 이름과 호출할 때 사용되는 인자 라벨을 가진다.

그러나 이니셜라이저는 함수처럼 이름이 없다. 그래서 함수처럼 이름으로 구분하는 것이 아니라 이니셜라이저의 파라미터 이름과 타입으로 식별한다. 그렇기 때문에 Swift는 이니셜라이저에서 레이블을 지정하지 않는 경우 자동으로 파라미터 이름인자 이름으로 제공한다.

다음 예제는 3개의 프로퍼티를 가지는 구조체 Color을 정의하였고, 이 구조체는 2개의 이니셜라이저를 가진다. 첫 번째 이니셜라이저는 3개의 파라미터를 가지고 2번째 이니셜라이저는 1개의 파라미터를 가진다.

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

2개의 이니셜라이저는 새로운 인스턴스를 만들고 각각의 이니셜라이저 파라미터에 값의 이름(red, green, blue, white)을 제공한다.

let magenta = Color(red: 1.0, green: 0.0, blue: 1.0)
let halfGray = Color(white: 0.5)

이니셜라이저 내에서 인자 라벨이 정의되어 있다면 반드시 호출될 때 사용되어야 한다. 그래서 다음 코드는 compile-time 에러를 발생시킨다.

let veryGreen = Color(0.0, 1.0, 0.0)
// 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(옵셔널 프로퍼티 타입)

저장 프로퍼티의 타입이 논리적으로 값이 없는 상태를 가질수 있고, 초기화 과정에서 값이 할당될 수 없거나 나중에 값이 할당되는 경우는 optional type으로 선언한다. 옵셔널 타입의 프로퍼티는 자동적으로 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프로퍼티는 옵셔널 타입으로 선언되었다.


Assigning Constant Properties During Initialization (초기화 중에 상수 프로퍼티 할당)

초기화 중에는 상수 프로퍼티에 값을 할당하는 것이 가능하다. 상수 프로퍼티에 값이 할당되면 값은 더 이상 변경될 수 없다.

NOTE
클래스 인스턴스의 경우 상수 프로퍼티는 해당 클래스에서의 초기화 과정에서만 값을 할당할 수 있고 서브 클래스에서는 값을 변경할 수 없다.

다음은 위 설문조사 예제에서 변수인 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.)"

위 예제를 확인해 보면 상수의 값이 초기화 과정에서 할당되는것을 확인할 수 있다.


Default Initializers (기본 이니셜라이저)

Swift는 클래스나 구조체의 모든 프로퍼티의 값이 default value로 할당되어 있고 이니셜라이저를 1개도 정의하지 않았다면, default initializers를 제공한다. 기본 이니셜라이저는 모든 프로퍼티가 기본 값으로 할당된 새로운 인스턴스를 만든다.

다음 예제는 클래스 ShoppingListItem을 정의하였다. 이 클래스의 모든 프로퍼티는 기본값을 가진다.

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

name프로퍼티는 optional type이므로 nil로 초기화된다. 그리고 ShoppingListItem()을 통해 기본 이니셜라이저를 호출하여 모든 프로퍼티가 기본 값을 가진 인스턴스를 생성하여 item 변수에 할당하였다.


Memberwise Initializers for Structure Types (구조체타입을 위한 멤버와이즈 이니셜라이저)

구조체 타입은 custom(사용자 정의) 이니셜라이저를 정의하지 않으면 자동적으로 memberwise initializer를 가진다. Default initializer(기본 이니셜라이저)와 다르게, 구조체는 stored properties(저장 프로퍼티)가 기본 값을 가지지 않더라도 memberwise initializer를 가진다.

Memberwise initializer는 새로운 구조체 인스턴스의 멤버 프로퍼티를 초기화하는 축약된 방법으로 인스턴스 프로퍼티의 초깃값은 파라미터의 이름으로 할당될 수 있다.

아래 예제는 2개의 프로퍼티를 가지는 구조체 Size를 정의하였고 프로퍼티의 기본 값은 0.0으로 할당하였다.

Size 구조체는 자동적으로 memberwise initializer인 init(width:height:)를 가지고 이는 새로운 구조체 인스턴스를 초기화 시킨다.

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

Memberwise initalizer를 호출할 때 기본값이 있는 프로퍼티는 값을 생략할 수 있다. 위 예제에서는 2개 프로퍼티 모두 기본 값을 가지기 때문에 둘 다 생략하거나 2개 중 1개만 인자로 값을 넘겨줄 수 있다. 이를 아래 예제에서 확인해 보자.

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"

그런데 여기서 1가지 의문이 들 수도있다. 구조체에서 Memberwise initializer는 기본 이니셜라이저와 다르게 프로퍼티의 기본값이 할당되어 있지 않아도 자동으로 생성된다고 하였다. 공식 문서에서는 기본값이 있는 예제만 다루기에, 기본값이 없는 구조체를 만들고 Memberwise initializer가 생성되는지 확인해 본다.

struct Size {
    var width: Double, height: Double
}

기본 값을 할당하지 않는 프로퍼티를 가지는 구조체를 선언하였다.

let twoByTwo = Size(width: 2.0, height: 2.0)
print(twoByTwo.width, twoByTwo.height)
// Prints "2.0 2.0"

기본값이 없어도 멤버와이즈 프로퍼티가 작동하는 것을 확인할 수 있다.

let zeroByTwo = Size(height: 2.0) // error
let zeroByZero = Size() // error

프로퍼티의 기본 값이 없어, 인자를 생략한 이니셜라이저 호출은 컴파일 에러가 발생하는 것을 확인할 수 있다.


Initializer Delegation for Value Types (값 타입을 위한 이니셜라이저 위임)

NOTE
아니셜라이저는 다른 이니셜라이저를 호출하여 인스턴스의 초기화를 수행할 수 있다. 이를 Initializer Delegation(이니셜라이저 위임)이라고 하고 다중 이니셜라이저 간의 코드 중복을 최소화 한다.

값 타입과 클래스 타입(참조 타입)은 이니셜라이저 위임 방식이 다르다. 값 타입(구조체와 열거형)은 상속을 지원하지 않아 이니셜라이저를 자기 자신의 이니셜라이저에게만 동작을 위임할 수 있기 때문에 비교적 간단하다. 그러나 다른 클래스를 상속하는 서브클래스는 슈퍼클래스에서 상속받은 저장 프로퍼티가 적절한 값으로 할당되도록 하는 책임이 있다.

값 타입에서 사용자 정의 이니셜라이저를 작성할 때 같은 값 타입의 다른 이니셜라이저를 호출하기 위해 self.init를 사용한다. self.init는 이니셜라이저 구문 내에서만 사용이 가능하다.

만약 값 타입에서 사용자 정의 이니셜라이저를 정의했다면, 기본 이니셜라이저나 멤버와이즈 이니셜라이저에 더 이상 접근할 수 없다. 이러한 제약은 이니셜라이저의 복잡성을 낮추고, 의도치 않는 사용을 방지한다.

NOTE
기본 이니셜라이저와 멤버와이즈 이니셜라이저를 사용자 정의 이니셜라이저와 같이 사용하려면 사용자 정의 이니셜라이저를 값 타입의 선언 구문 내에서 구현하지 말고 extension(익스텐션)에서 구현하도록 한다.

다음은 값 타입의 2개의 구조체를 선언하였다.

struct Size {
    var width = 0.0, height = 0.0
}

struct Point {
    var x = 0.0, y = 0.0
}

Rect 구조체를 3개의 방법으로 초기화 시킬 수 있다. 이 3가지 방법 모두 사용자 정의 이니셜라이저이다.
1. init()
2. init(origin: Point, size: Size)
3. init(center: Point, size: 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()는 구조체의 기본 이니셜라이저와 기능적으로 동일하다.구조체 Rect의 프로퍼티는 Point, Size 타입이고 이 2개의 타입은 모든 프로퍼티가 기본 값을 가지고 있기 때문에 인자 없이 이니셜라이저를 호출할 수 있다. 그렇기에 init()를 호출하면 기본값을 가지는 Rect 구조체의 인스턴스가 반환된다.

let basicRect = Rect()
// basicRect's origin is (0.0, 0.0) and its size is (0.0, 0.0)

두 번째 이니셜라이저 init(origin:size:)는 구조체의 멤버와이즈 이니셜라이저와 기능적으로 동일하다. 이는 인자를 통해 프로퍼티에 적절한 값을 할당할 수 있다.

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의 값을 기반으로 원래의 지점을 찾는다. 그 다음 2번째 이니셜라이저 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)

Class Inheritance and Initialization (클래스 상속과 초기화)

슈퍼클래스에서 상속한 프로퍼티를 포함하여 모든 저장 프로퍼티는 초기화 단계에서 초깃값이 할당돼야 한다.

Swift는 클래스 타입에서 모든 저장 프로퍼티가 초깃값을 가지는 2가지 이니셜라이저를 정의한다. 이는 designated initializers(지정 이니셜라이저), convenience initializers(편리 이니셜라이저)이다.

Designated Initializers and Convenience Initializers (지정 이니셜라이저와 편리 이니셜라이저)

Designated initializers(지정 이니셜라이저)는 클래스에서 중요한 이니셜라이저다. Designated initializers는 클래스의 모든 프로퍼티를 초기화 시키고 체인으로 연결된 슈퍼클래스의 이니셜라이저를 계속해서 호출한다. 클래스는 반드시 1개 이상의 designated initializer를 가진다. 서브클래스가 상위 클래스의 Designated initializers를 상속함으로써 해당 조건을 만족시킬 수 도 있다.

Convenience initializers는 클래스에서 보조 이니셜라이저로 같은 클래스의 designated initializers의 파라미터의 일부를 default value로 할당하여 호출할 수 있다.


Syntax for Designated and Convenience Initializers (지정 이니셜라이저와 편리 이니셜라이저의 문법)

클래스의 designated initializers는 값 타입의 단순한 이니셜라이저와 동일한 구문을 가진다.

init(parameters) {
    statements
}

Convenience 이니셜라이저는 기본 이니셜라이저와 비슷하지만 앞에 convenience 키워드를 추가로 작성한다.

convenience init(parameters) {
    statements
}

Initializer Delegation for Class Types (클래스 타입의 이니셜라이저 위임)

Designated initializers와 convenience initializers 관계를 단순히 하기 위해 swift는 이니셜라이저 간의 위임 호출에 대해 3가지 규칙을 적용한다.

Rule 1: Designated initializers는 직계(조부모가 아닌 부모) 슈퍼클래스의 Designated initializers를 반드시 호출해야 한다.

Rule 2: Convenience initializers는 반드시 같은 클래스에서 다른 이니셜라이저를 호출해야 한다.

Rule 3: Convenience initializers는 궁극적으로 designated initializers를 호출해야 한다.

기억하기 쉽게 요약하면 다음과 같다.

  • designated initializers는 반드시 위임을 상위 클래스로 해야한다.
  • Convenience 이니셜라이저는 반드시 위임을 같은 클래스에서 해야한다.

이러한 규칙을 그림으로 표현하면 다음과 같다.

그림을 보면 상위클래스는 1개의 Designated initializers와 2개의 convenience 이니셜라이저를 가지고 있다. 1개의 convenience 이니셜라이저는 다른 이니셜라이저를 호출하고 이 이니셜라이저는 designated initializers를 호출한다. 이는 규칙2와 규칙3을 만족한다.

하위클래스는 2개의 Designated initializers를 가지고 1개의 convenience 이니셜라이저를 가진다. 1개의 convenience 이니셜라이저는 2개중 1개의 designated initializers를 호출하는데, 이는 같은 클래스에서 오직 1개의 다른 이니셜라이저를 호출할 수 있기 때문이다. 그리고 2개의 Designated initializers는 상위클래스의 designated initializers를 호출함으로 규칙1,2,3을 모두 만족한다.

NOTE
이니셜라이저 호출의 모양이 반드시 위처럼 될 필요는 없다. 다만 위에서 언급한 3개의 규칙만 지키면 된다.

아래 그림은 좀 더 복잡한 4개 클래스의 계층 구조를 보여준다. Designated initializers가 클래스들의 chain(연쇄)에서 상위클래스로 연결시키는 지점인 것을 확인할 수 있다.


Two-Phase Initialization (2단계 초기화)

Swift에서 클래스 초기화는 2단계로 진행된다. 첫 번째 단계에서 각각의 저장 프로퍼티는 초깃값으로 할당된다. 모든 저장 프로퍼티의 초기 상태가 결정되면 2번째 단계가 시작된다. 2번째 단계는 새로운 인스턴스를 사용하기 전에 저장 프로퍼티를 customize 하는 단계다.

2단계의 초기화 과정은 초기화를 안전하게 수행하고 클래스들 간에 유연성을 제공한다. 프로퍼티가 초기화되기 전에 접근되는 것을 방지하고, 프로퍼티가 예기치 않게 다른 이니셜라이저에 의해 다른 값이 할당되는 것을 방지한다.

NOTE
Swift의 2단계 초기화는 Objective-C와 유사하다. 중요한 차이점은 Objective-C의 1단계에서 nil이나 0으로 프로퍼티의 초깃값을 할당하는 반면, Swift는 custom values를 할당할 수 있다.

Swift의 컴파일러는 2단계 초기화가 에러 없이 수행되는 것을 보장하기 위해 4단계의 safety-check(안전 확인)을 한다.

  1. 모든 designated initializers는 슈퍼클래스의 이니셜라이저에게 초기화를 위임하기 전에 클래스의 모든 프로퍼티를 초기화해야 한다. 위에서 언급했듯이 객체의 메모리는 모든 저장된 프로퍼티가 초기 상태를 갖춰야 완전히 초기화 된것으로 간주한다. 이 규칙을 따르기 위해, 다른 이니셜라이저에게 chain을 건네주기 전에 자신의 모든 프로퍼티가 초기화되었는지 확인해야 한다.
  1. Designated initializers는 상속된 프로퍼티에 값을 할당하기 전에 슈퍼클래스의 이니셜라이저에게 초기화를 위임해야한다. 그렇지 않은 경우 상속된 값이 슈퍼클래스의 초기화에 의해서 덮여 쓰여지게 된다.

  2. Convenience initializers는 어떤 프로퍼티에게 값을 할당하기 전에 다른 이니셜라이저에게 위임해야한다. 그렇지 않으면 convenience 초기자에 의해 할당된 값을 다른 클래스의 지정 초기자에 의해 덮여 쓰이게 된다.

  3. 이니셜라이저는 초기화의 첫 번째 단계가 끝나기 전까지 인스턴스 메소드를 호출하거나, 인스턴스 프로퍼티의 값을 읽거나, 값으로 self를 참조할 수 없다.

여기에 위의 4가지의 safety-check를 기반으로 초기화를 진행하는 과정은 다음과 같다.

1 단계

  • 클래스에서 designated 또는 convenience initializers가 호출된다.
  • 새로운 클래스 인스턴스를 위한 메모리가 할당된다. 그러나 메모리는 아직 초기화되지 않았다.
  • 클래스의 designated initializers는 클래스의 모든 저장 프로퍼티가 값을 가졌는지 확인한다. 그 다음 저장 프로퍼티의 메모리가 초기화된다.
  • Designated initializers는 자신의 저장된 프로퍼티에 동일한 일을 수행하기 위해 슈퍼클래스의 이니셜라이저에게 위임한다.
  • 제일 최상위 클래스에 도달할 때까지 chain을 위로 전달한다.
  • Chain이 최상위 클래스에 도달하고, 마지막 클래스의 저장 프로퍼티가 값을 가졌는지 확인하면, 인스턴스의 메모리가 완전히 초기화 된것으로 간주되고 1단계가 완료된다.

2 단계

  • 클래스들의 체인을 되돌아가면서, 각각의 designated 이니셜라이저는 인스턴스를 customize 할 수 있다. 이니셜라이저는 self에 접근할 수 있고 프로퍼티의 값을 수정할 수 있으며 인스턴스 메소드를 호출할 수 있다.
  • 마지막으로 체인의 convenience initializers는 자기 자신의 인스턴스를 customize 할 수 있다.

다음은 2단계 초기화를 그림으로 표현하였다.


다음 내용은 뒷 포스트에 이어서 작성한다. initialization(2)

0개의 댓글