[Swift] initialization (2)

LEEHAKJIN-VV·2022년 5월 19일
0

Study-Swift 5.6

목록 보기
16/22

NOTE
위 글은 [Swift] initialization (1) 포스트를 이어서 작성한다.

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


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

Initializer Inheritance and Overriding (이니셜라이저 상속과 오버라이딩)

Objective-C와 다르게 Swift의 서브클래스는 슈퍼클래스의 이니셜라이저를 기본적으로 상속하지 않는다. 슈퍼클래스의 이니셜라이저가 무분별하게 상속되어 복잡하게 되고 잘못 초기화 되는 상항을 예방하기 위해서이다.

NOTE
슈퍼클래스의 이니셜라이저는 안전하고 꼭 필요한 경우에만 상속된다.

서브클래스에서 슈퍼클래스의 designated initializer를 구현하려면 override 키워드를 추가하고 서브클래스 내에서 정의하여 이니셜라이저를 오버라이드 할 수 있다. 물론 슈퍼클래스의 default initializers(기본 이니셜라이저)도 마찬가지이다.

슈퍼클래스의 convenience initializer를 서브클래스에서 구현하는 경우, 서브 클래스에서 슈퍼클래스의 이니셜라이저를 직접 호출할 수 없다. 그러므로 서브클래스는 슈퍼클래스의 이니셜라이저를 override할 수 없다. 결과적으로 슈퍼클래스의 convenience initializer를 서브클래스에서 구현하는 경우 override 키워드를 작성하지 않는다.

아래 예제는 base class인 Vehicle을 정의하였다. 클래스는 1개의 저장 프로퍼티와 1개의 computed property를 가진다.

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

NOTE
designated initializers는 클래스의 모든 프로퍼티를 초기화하고, convenience initializers는 default value를 제공하여 designated initializers를 보조한다.

Vehicle 클래스는 저장 프로퍼티에 기본값을 제공하고 다른 custom initializers를 정의하지 않았다. 그래서 클래스는 자동적으로 default initializers를 가진다. Default initializer는 항상 designated initializers이고 인스턴스를 생성하는 역할을 가진다.

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

다음 예제는 Vehicle의 서브클래스인 Bicycle을 정의하였다.

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

Bicycle 서브클래스는 custom designated initializers인 init()를 정의하였다. 이 designated initializer는 슈퍼클래스의 이니셜라이저를 오버라이드 하기 때문에 override 키워드를 작성한다.

init() 이니셜라이저는 super.init()를 호출하는 데, 이는 슈퍼클래스의 default 이니셜라이저를 호출한다. 이렇게 구현하면 서브클래스가 상속된 프로퍼티에 접근하기 전에 슈퍼클래스에 의해 초기화된 것을 보장할 수 있다.

Bicycle 클래스의 인스턴스를 만들고 computed property를 호출하면 서브클래스에서 상속된 프로퍼티가 수정된 값을 가지고 있는 것을 확인할 수 있다.

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

다음은 Vehicle을 상속하는 다른 클래스 Hoverboard를 정의하였다. 이 클래스의 이니셜라이저는 오직 서브클래스의 프로퍼티인 color에게만 값을 할당한다.

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

또한 슈퍼클래스의 computed property의 getter를 override하였다.

Hoverboard 클래스의 인스턴스는 Vehicle 이니셜라이저에 의해 초기화 된 default 값(super.description)을 사용하는 것을 확인할 수 있다.

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

NOTE
서브클래스의 이니셜라이저에서 var(변수) 프로퍼티는 변경이 가능하지만 let(상수)는 변경할 수 없다.


Automatic Initializer Inhertiance (이니셜라이저 자동 상속)

위에서 언급한 것처럼 서브클래스는 슈퍼클래스의 이니셜라이저를 기본적으로(자동으로) 상속하지 않는다. 그러나 슈퍼클래스의 이니셜라이저는 특정 상황에서 자동으로 상속되고 이는 생각보다 많이 발생한다. 서브클래스에서 새로 추가한 모든 프로퍼티에 default value를 할당하면 다음 2가지 규칙을 따른다.

  1. 서브클래스가 어떠한 designated initializer를 정의하지 않으면 자동으로 슈퍼클래스의 모든 designated initializers를 상속한다.

  2. 서브클래스가 슈퍼클래스의 모든 designated initalizers를 구현하거나, 규칙 1에 따라 모든 designated initalizers를 상속하면 자동적으로 슈퍼클래스의 convenience initializers를 상속한다.

이러한 규칙은 서브클래스가 convenience initializers를 추가하는 경우에도 적용된다.

NOTE
규칙2 에 따라 서브클래스는 슈퍼클래스의 designated initializer를 서브클래스의 convenience initializer로 구현 가능하다.


Designated and Convenience Initializers in Action (지정 이니셜라이저와 편리 이니셜라이저의 동작)

다음 예제는 3가지 이니셜라이저인 designated initializers, convenience Initializers, 자동으로 상속된 initializers의 동작을 보여준다. 이 예제는 3개의 클래스가 서로 상속관계에 있다.

Base class(최상위 클래스)인 Food클래스는 식품의 이름을 캡슐화한다. 이 클래스는 1개의 프로퍼티를 가지고, 인스턴스를 생성하는 2개의 이니셜라이저를 가진다.

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

아래 그림은 Food 클래스의 이니셜라이저 chain을 보여준다.

클래스는 자동으로 memberwise initializer를 가지지 않는다. 그래서 Food 클래스는 1개의 파라미터를 가지는 designated initializer을 정의한다. 이 이니셜라이저는 특정한 이름을 사용하여 새로운 인스턴스를 만든다.

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

Food클래스의 init(name: String) 이니셜라이저는 designated initializer이다. 새로운 Food 인스턴스의 모든 저장 프로퍼티를 초기화 시키기 때문이다. Food 클래스는 최상위 클래스이므로 init(name: String) 이니셜라이저는 초기화를 완료하기 위해 super.init()를 작성할 필요가 없다. (2단계 초기화 safety-check)

또한 Food 클래스는 convenience 이니셜라이저인 파라미터가 없는 init()를 가지고 있다. 이 이니셜라이저는 다른 이니셜라이저(init(name: String))에게 초기화를 위임한다.

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

2번째 클래스 RecipeIngredientFood클래스를 상속한다. 이 클래스는 자체 프로퍼티 1개 가지고, 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 클래스의 이니셜라이저 chain을 보여준다.

RecipeIngredient 클래스는 1개의 designated initializer인 init(name: String, quantity: Int)를 가진다. 이 이니셜라이저는 quantity 프로퍼티에 값을 할당한 다음 슈퍼클래스의 init(name:String) 이니셜라이저에게 위임한다. 이는 2단계 초기화의 safet-check의 첫 번째를 만족한다.

또한 RecipeIngredient 클래스는 convenience initializer init(name:String)을 정의하였다. 이는 name 프로퍼티는 인자로 값을 전달받고 quantity프로퍼티는 default value를 제공한다. 즉 같은 클래스의 designated initializer에게 초기화를 위임한다.

RecipeIngredient 클래스의 convenience initializer는 슈퍼클래스의 designated initializer를 override 한다. 즉 서브클래스가 슈퍼 클래스가 모든 designated initializer를 구현했으므로 자동으로 슈퍼클래스의 convenience initializer가 상속된다.

새로운 RecipeIngredient 인스턴스를 생성할 수 있는 3가지의 이니셜라이저가 있다.

let oneMysteryItem = RecipeIngredient()
print(oneMysteryItem.name, oneMysteryItem.quantity)
// Prints "[Unnamed] 1"

위와 같이 인자가 없이 인스턴스를 생성하면 다음과 같은 과정으로 초기화가 일어난다.

  1. 슈퍼클래스로 부터 상속받은 init() 호출
  2. init가 자신의 convenience initializer에게 초기화 위임
  3. convenience initializer가 자신의 designated initializer에게 초기화 위임
  4. RecipeIngredient클래스의 designated initializer가 슈퍼클래스 food의 designated initializer에게 초기화 위임

나머지 2개의 이니셜라이저 또한 위 그림처럼 동작하니 직접 한번 확인하는 것을 추천한다.

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

마지막 클래스로 서브클래스 RecipeIngredient를 상속하는 ShoppingListItem를 정의한다. 이 클래스는 Boolean 프로퍼티와 computed property를 각각 1개씩 추가로 구현하였다.

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

NOTE
ShoppingListItem 클래스에서 purchase 프로퍼티 값은 항상 초깃값을 가지기 때문에 어떠한 이니셜라이저도 구현하지 않았다.

ShoppingListItem 클래스는 모든 프로퍼티에 default value를 할당하고, 그리고 어떠한 이니셜라이저도 정의하지 않았기 때문에 자동적으로 슈퍼클래스의 모든 designated 그리고 convenience initializers를 상속한다. (이니셜라이저의 자동 상속 2가지 규칙)

이 3가지 클래스의 이니셜라이저 chain은 다음과 같다.

ShoppingListItem 클래스에 상속된 3개의 이니셜라이저를 통해 인스턴스를 생성하는 예제를 확인해 보자.

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 ✘

Failable Initializers (실패 가능한 이니셜라이저)

초기화가 실패할 수 있는 클래스, 구조체, 열거형을 정의하는 것이 유용한 경우가 있다. 초기화 실패는 부적절한 초기화 파라미터 값, 필요한 외부 리소스 부재, 초기화 성공을 방해하는 기타 조건에 의해 발생될 수 있다.

Failable initializer(실패 가능한 이니셜라이저)는 init() 키워드 뒤에 (?) 물음표 문자를 추가하여 구현한다.

NOTE
Failable initializer와 nonfailable(실패 불가능한) 이니셜라이저의 파라미터의 타입과 파라미터 이름을 같게 선언할 수 없다. 이유는 이니셜라이저는 이름으로 구분하는 것이 아닌 파라미터 타입과 파라미터 이름으로 구분하기 때문이다.

Failable initializer는 optional value를 생성한다. 초기화가 실패하는 것을 나타내기 위해 failable initializer 내에서 return nil을 작성한다.

NOTE
엄격히 말하자면, 이니셜라이저는 값을 반환하지 않는다. 이니셜라이저의 역할은 초기화가 끝날때 까지 self가 올바르고 완전하게 초기화 되도록 하는 것이다. 초기화 실패를 나타내기 위해 return nil을 작성하지만 초기화 성공을 나타내기 위해 return 키워드를 작성하지 않는다.

다음 예제는 failable initializer가 숫자형 타입의 변환을 위해 구현되었다. 숫자형 타입 간의 변환이 값을 정확하게 유지하기를 원한다면 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"

아래 예제는 구조체 Animal을 정의한다. 이 구조체는 1개의 문자열 상수 프로퍼티와 1개의 파라미터를 가지는 failable initializer를 구현한다. 이 이니셜라이저는 species 프로퍼티 값이 빈 문자열인지 확인하고 빈 문자열이면 nil을 반환한다.

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"

만약 failable initializer의 파라미터에 빈 문자열을 전달하면 이니셜라이저는 초기화 실패를 trigger 한다.

let anonymousCreature = Animal(species: "")
// anonymousCreature is of type Animal?, not Animal

if anonymousCreature == nil {
    print("The anonymous creature couldn't be initialized")
}
// Prints "The anonymous creature couldn't be initialized"

NOTE
빈 문자열("")와 nil은 다르다. 빈 문자열은 문자열이 비어있다는 의미로 그 자체가 값이 된다. 그러나 nil은 문자열의 값이 존재하지 않는 상태를 의미한다. 위 예제에서는 동물의 이름이 없는 경우는 없기 때문에 빈 문자열이 발견이 되면 이니셜라이저가 초기화 실패를 trigger 한다.


Failable Initializers for Enumerations (열거형에서 실패가능한 이니셜라이저)

Failable Initializer를 1개 이상의 파라미터를 기반으로 사용하는 열거형 case를 선택하는 데 사용할 수 있다. 파라미터에 값이 전달된 다음, 열거형의 어떠한 case에도 일치하지 않으면 이니셜라이저는 실패를 trigger 한다.

아래 예제는 열거형 TemperatureUnit을 정의하였고, 3개의 case를 구현하였다. Failable initializer은 적절한 온도 단위를 나타내는 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
        }
    }
}

Failable initializer을 사용하여 3가지 가능한 상태를 열거형의 case로 선택하게 한다. 그리고 파라미터가 3개 중 1개의 상태와도 일치하지 않는다면 초기화는 실패하여 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 isn't a defined temperature unit, so initialization failed.")
}
// Prints "This isn't a defined temperature unit, so initialization failed."

Failable Initializers for Enumerations with Raw Values

Raw value(원시 값)을 가지는 열거형은 자동으로 failable initializer인 init(rawValue:)을 가진다. 이니셜라이저의 파라미터는 raw-value 타입의 rawValue라는 파라미터를 사용한다. 파라미터가 열거형의 case와 일치하는 값이 없으면 초기화 실패를 trigger 한다.

위 예제를 다시 작성하여 열거형 TemperatureUnitinit?(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 isn't a defined temperature unit, so initialization failed.")
}
// Prints "This isn't a defined temperature unit, so initialization failed."

Propagation of Initialization Failure (초기화 실패 전파)

클래스, 구조체, 열거형의 failable initializer은 같은 클래스의 다른 failable initializer에게 위임할 수 있다. 마찬가지로 서브클래스의 failable initializer은 슈퍼클래스의 failable initializer에게 위임할 수 있다.

두가지 경우 모두, 초기화 실패를 발생시키는 이니셜라이저에게 위임하면, 전체 초기화 과정은 즉시 중단되고, 그리고 더이상 코드는 실행되지 않는다.

NOTE
Failable initializer은 nonfailable initializer에게 위임할 수 있다. 기존에 존재하는 초기화 과정에서 잠재적으로 실패가 발생할 가능성이 있을 경우 이 방법을 사용한다.

아래 에제는 Product를 상속하는 CarItem을 정의하였다. CarItem은 1개의 저장 프로퍼티를 가지고 이 값이 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)
    }
}

CartItem의 failable initializer는 quantity 프로퍼티의 값이 1보다 작은지 확인하다. 만약 1보다 작은 유효하지 않은 값이 전달되면 전체 초기화 과정은 실패되고 코드는 더 이상 진행되지 않는다. 마찬가지로, Product의 failable initializer도 name 프로퍼티의 값이 빈 문자열이면 즉시 초기화를 trigger 한다.

아래 코드처럼 비어있지 않은 name과 1이상 값을 quantity에 전달하면 초기화는 성공한다.

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

name은 유효한 값, quantity는 1미만인 값인 0을 전달하면 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 프로퍼티의 값에 빈 문자열을 전달하면 슈퍼클래스 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 (실패 가능한 이니셜라이저 오버라이딩)

서브클래스에서 다른 이니셜라이저와 마찬가지로 슈퍼클래스의 failable initializer를 override(재정의)할 수 있다. 대신에, 슈퍼클래스의 failable initializer를 서브클래스에서 nonfailable initializer로 override 해야 한다.

슈퍼클래스의 failable initializer를 서브클래스의 nonfailable initializer로 override 하는 경우, 슈퍼클래스의 이니셜라이저에게 위임하는 유일한 방법은 슈퍼클래스의 failable initializer 결과를 force-unwrap하는 것이다.

NOTE
failable initializer를 nonfailable initializer로 override할 수 있지만 그 반대는 불가능하다.

아래 예제는 클래스 Document를 정의하였다. 이 클래스는 비어있지 않은 문자열이거나, nil인 2가지 상태를 가진다.

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를 선언한다. 이 클래스는 슈퍼클래스의 designated initializers를 모두 상속한다. name 없이 초기화되거나, 빈 문자열이 전달되면 AutomaticallyNamedDocument 인스턴스 name 프로퍼티의 초깃값이 [Untitled]이 되도록 override 하였다.

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는 슈퍼클래스의 failable initializer인 init?(name:)을 nonfailable initializer인 init(name:)로 override 한다. AutomaticallyNamedDocument 클래스는 슈퍼클래스와 다른 방법으로 빈 문자열을 처리하여, 이 클래스의 이니셜라이저는 실패가 필요 없고, 그래서 nonfailable 버전의 이니셜라이저로 구현했다.

또한 override init(name: String) 이니셜라이저를 살펴보면 첫 줄에 super.init()을 호출하는 것을 확인할 수 있다. 이는 다음 2가지 이유에 의해서 작성되었다. (아래 적용된 규칙은 initialization(1)에서 확인할 수 있음)

  • 서브클래스의 designated initializer는 반드시 슈퍼클래스의 designated initializer를 호출해야 한다. (클래스 타입 간의 이니셜라이저 위임 규칙1)

  • Designated initializer는 상속된 프로퍼티에 값을 할당하기 전에 슈퍼클래스의 이니셜라이저에게 초기화를 위임해야 한다. (2단계 초기화의 safe-check)

서브클래스의 nonfailable initializer 구현 내부에서 forecd unwrapping(강제적 언래핑)을 사용하여 failable initializer를 호출할 수 있다. 예를 들어 서브클래스 UntitledDocument는 항상 이름이 [Untitled]이며 슈퍼클래스의 failable initializer인 init(name:)을 호출한다.

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

만약 위의 예제에서 super.init(name: "[Untitled]")! 대신 super.init(name: "")!을 호출한다면 슈퍼클래스에서 nil이 반환되기 때문에 runtime error가 발생한다.


The init! Failable Initializer (실패가능한 init!)

Optional 인스턴스를 반환하는 failable initializer인 init?를 주로 사용한다. 암시적으로 optional 인스턴스를 unwrapped 하는 failable initializer를 정의할 수 도 있다. 이 이니셜라이저는 물음표(?) 대신 느낌표(!)을 사용한다.

init?에서 init!로 초기화를 위임할 수 있고, 반대도 마찬가지다. 또한 init?init!로 override 할 수 있다.


Requried Initializers (필수적인 이니셜라이저)

이니셜라이저의 선언 앞에 required 키워드를 작성하면 이 클래스를 상속하는 서브클래스에서 이 이니셜라이저를 반드시 구현해야 한다.

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

이 슈퍼클래스를 상속한 서브클래스에서도 반드시 required 키워드를 붙여 상속관계에 있는 다른 서브클래스에게도 알려야한다.

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

NOTE
requried initializer를 반드시 내부 코드를 구현할 필요는 없다.


Setting a Default Property Value with a Closure or Function (클로저나 함수를 이용해 기본 값 할당)

만약 저장 프로퍼티의 default value가 복잡한 계산이 필요로 한다면, 클로저나 전역 함수를 사용하여 프로퍼티의 default value를 할당할 수 있다. 프로퍼티를 가진 인스턴스가 초기화될 때마다 클로저나 함수는 호출되고, 클로저나 함수가 반환한 값은 프로퍼티의 default value로 할당된다.

다음은 클로저를 사용하여 프로퍼티에 기본 값을 할당하는 기본 개요를 보여주는 예제다.

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가 클로저를 즉시 실행하도록 지시한다. 만약 괄호를 생략하면 클로저는 값을 반환하는 것이 아니라 프로퍼티에 클로저 자체가 할당된다.

NOTE
클로저를 사용하여 프로퍼티를 초기화하는 경우, 인스턴스의 나머지 부분이 아직 초기화가 되지 않은 상태이다. 즉 클로저 내부에서 다른 프로퍼티나 self에 접근할 수 없다.

아래 예제는 구조체 Chessboard을 정의하였다. 이 체스보드는 흰색과 검은색이 섞인 8*8의 크기를 가진다.

이 체스보드를 나타내기 위해서 1개의 프로퍼티를 선언한다. 이 프로퍼티는 64개의 Bool 값을 가진다. 배열의 true 값은 검은 사각형을 나타내고, false 값은 흰색 사각형을 나타낸다. 이 배열은 1차원 배열로 시작은 왼쪽 최상단 위에서 시작하여 행 방향으로 읽는다.

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

Chessboard 인스턴스가 생성될 때 클로저는 실행되고, 클로저의 반환 값이 boardColors 프로퍼티에 할당된다. 클로저 내부에서 임시 배열 temporaryBoard가 적절한 값을 가지고 생성되고, 클로저는 이를 반환하여 프로퍼티 boardColors에 할당한다.

let board = Chessboard()
print(board.squareIsBlackAt(row: 0, column: 1))
// Prints "true"
print(board.squareIsBlackAt(row: 7, column: 7))
// Prints "false"

squareIsBlackAt(row:column:) 함수를 이용해 할당된 프로퍼티의 값을 확인할 수 있다.

0개의 댓글