티스토리 뷰

 초기화는 클래스, 구조체, 열거형 인스턴스를 사용하기 위한 준비 작업이다. 이 과정에서 각 저장 프로퍼티의 초기 값을 설정하며, 초기화는 initailizer를 정의하는 것으로 구현할 수 있다.

 

바로 확인해보자.


1. 저장 프로퍼티를 위한 초기값 설정

인스턴스의 저장 프로퍼티는 사용하기 전에 반드시 특정 값으로 초기화 되어야 한다. 

 

- Initializers 

 Initializer는 특정 타입의 인스턴스를 생성한다. Initializer 의 가장 간단한 형태는 파라미터가 없는 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")

 

- 기본 프로퍼티

프로퍼티의 선언과 동시에 값을 할당한다면, 그 값을 바로 초기 값으로 사용할 수 있다.

struct Fahrenheit {
    var temperature = 32.0
}

2. Customizing Initialization

 초기화 프로세스를 입력 값과 옵셔널 프로퍼티 타입 혹은 상수 값을 할당하여 커스터마이징 할 수 있다.

 

- Initialization Parameters

struct Celsius {
    var temperatureInCelcius: Double
    
    init(fromFahrenheit fahrenheit: Double) {
        temperatureInCelcius = (fahrenheit - 32.0) / 1.8
    }
    
    init(fromKevin kelvin: Double) {
        temperatureInCelcius = 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

 

- 파라미터 이름과 인자 레이블

 메서드 파라미터와 초기화 파라미터 모두 파라미터 이름과 레이블을 가질 수 있지만, Initializer 는 특정 메서드에서 지정하는 메서드 이름을 지정하지 않고 실별자로 파라미터를 사용한다. 

모든 파라미터는 인자 레이블을 갖는데 만약 사용자가 이 레이블을 지정하지 않으면 Swift가 자동으로 하나를 할당하여 제공한다.

 

struct Color {
    let red, green, blue: Double
    
    init(red: Double, green: Double, blue: Double) {
        self.red = red
        self.blue = blue
        self.green = green
    }
    init(white: Double) {
        red = white
        green = white
        blue = white
    }
}

위 코드에서 정의한데로 Color 인스턴스 인자 값 3개 혹은 하나를 이용하여 생성할 수 있는데, 인자 레이블을 포함하지 않는다면 컴파일 과정에서 에러가 발생한다.

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

 

- Optional Property Types 

프로퍼티의 최초 값이 없고 나중에 추가될 수 있는 값을 옵셔널로 선언하여 사용할 수 있다. (뭐... 당연한 이야기다)

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

cheeseQuestion.response = "Yes, I like'em"

print(cheeseQuestion.response!)

 

- 초기화 중에 상수 프로퍼티 할당

상수 프로퍼티는 초기화 후에 변경할 수 없는 것이지, 초기화가 불가능 하다는 뜻은 아니다.

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

3. Default Initializers

 

모든 프로퍼티의 초기 값이 설정되어 있고, 하나의 초기자도 정의하지 않았다면 Swift는 모든 프로퍼티를 기본 값으로 초기화 하는 기본 Initializer를 제공한다.

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

 

구조체 타입은 모든 파라미터를 인자로 사용하는 멤버 Initializer를 제공한다.

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

 

- 값 타입을 위한 Initializer 위임

Initializer에서 다른 Initializer를 호출할 수 있다. 이 과정을 Initializer 위임이라고 한다. 값 타입(구조체, 열거형)과 클래스 타입 간 Initializer 위임 동작이 다른데, 값 타입은 상속을 지원하지 않기 때문에 자신의 Initializer끼리만 위임이 가능하다.

하지만, 클래스 타입은 상속이 가능하기 때문에 부모 클래스의 Initializer를 자식 클래스에서 호출이 가능하다.

 

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

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)

4. Class Inheritance and Initialization

 

 모든 클래스의 저장 프로퍼티와 부모 클래스로부터 상속 받은 모든 프로퍼티는 초기화 단계에서 반드시 초기 값이 할당되어야 한다. 

Swift에서는 클래스 타입에서 모든 프로퍼티가 초기 값을 갖는 것을 보장하기 위해 2가지 방법을 지원한다.

 

- Designated Initializers and Convenience Initializers

 Designated Initializer(지정 초기자)는 클래스의 Primary 초기자이다. 지정 초기자는 클래스의 모든 프로퍼티를 초기화 하며, 클래스 타입은 반드시 한개 이상의 지정 초기자를 가지고 있어야 한다.

 

클래스의 지정 초기자 문법은 값 타입 초기자와 동일하다.

init(parameters) {
    statements
}

 

Convenience Initializer(편리한 초기자)는 초기화 단계에서 미리 지정 된 값을 사용하여 최소한의 입력으로 초기화를 할 수 있도록 해준다. Convenience Initializer 내에서 반드시 Designated Initializer가 호출되어야 한다.

 

편리한 초기자는 기본 초기자와 문법이 같지만 init 앞에  convenience 키워드가 붙는다.

convenience init(parameters) {
    statements
}

 

- 클래스 타입을 위한 Initializer 위임

지정 초기자와 편리한 초기자 사이의 관계를 단순화 하기 위해 Swift는 초기자를 위임하는 호출에 대한 3가지 규칙을 따른다.

 

 1) 지정 초기자는 반드시 직계 부모 클래스의 지정 초기자를 호출해야 한다.

 2) 편리한 초기자는 반드시 같은 클래스의 다른 초기자를 호출해야 한다.

 3) 편리한 초기자는 궁극적으로 지정 초기자를 호출해야 한다.

 

 - 2단계 초기화
 Swift에서 클래스 초기화는 2단계로 진행된다. 첫번째 단계에서는 각 저장된 프로퍼티가 초기 값으로 초기화 되며, 모든 저장된 프로퍼티의 상태가 결정되면 두번째 단계를 시작한다.

두번째 단계에서는 새로운 인스턴스의 사용이 준비됐다고 알려주기 전에 저장된 프로퍼티를 커스터마이징하는 단계이다.

 

- Initializer의 상속과 오버라이딩

 Swift에서는 기본적으로 자식클래스에서 부모클래스의 Initializer를 상속하지 않는다. 이유는 부모클래스의 초기자가 무분별하게 상속되어 자식클래스에서 이를 잘못 초기화하는 것을 막기 위해서이다.

 

만약 클래스에서 모든 프로퍼티의 초기 값이 지정되어 있고 아무런 커스텀 이니셜라이져를 선언하지 않았다면, 기본 이니셜라이져인 init()을 사용할 수 있다.

 

아래 예제는 클래스를 생성하고 해당 클래스의 자식 클래스에서 이니셜라이져를 오버라이드 하여 사용하는 예제이다.

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

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

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

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

 

- Automatic Initializer Inheritance

 앞서 위에서 언급한 내용과 같이 자식클래스는 부모클래스의 이니셜라이져를 기본적으로 상속하지 않는다. 하지만, 특정 조건이 만족되면 자동으로 상속 받는다. 

특정 조건은 다음과 같다.

 1) 자식클래스가 지정 초기자를 정의하지 않으면 자동으로 부모클래스의 모든 지정 초기자를 상속한다.

 2) 자식클래스가 부모 클래스의 지정 초기자를 모두 구현한 경우 자동으로 부모 클래스의 편리한 초기자를 추가한다.

 

아래 예제는 Automatic Initializer Inheritance의 예제이다.

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

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

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

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()
print(oneMysteryItem.name)
// [Unnamed]

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

위 코드에서 Food 클래스를 상속받는 RecipeIngredient 클래스는 부모 클래스의 모든 초기자를 구현하였다. 그렇기 때문에 인자 값이 없는 Food 의 convenience initializer또한 자동으로 상속되어 사용할 수 있는 것이다.

 

아래 예제를 하나 더 보자.

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 ✘

ShoppingListItem 클래스는 아무런 부모 클래스의 이니셜라이저를 구현하지 않았지만, 자신의 프로퍼티들을 모두 초기화한 상태이기 때문에 부모 클래스의 모든 이니셜라이저를 사용할 수 있다.


5. Failable Initializers

 초기화 과정 중에 실패할 가능성이 있는 이니셜라이져를 init 뒤에 물음표(?)를 사용하여 실패가 가능한 이니셜라이져라고 표시할 수 있다.

 

예제를 한번 확인해보자.

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"

위 코드는 Int 형에 정의되어 있는 Failable 이니셜라이저를 사용한 예제이다. Int(exactly:) 초기자는 Int를 반환하는 것이 아닌 옵셔널 Int를 반환하는 것을 확인할 수 있다.

 

아래 예제는 이니셜라이저에 입력 값이 없으면 초기화 실패가 발생하도록 구현한 예제이다.

 

struct Animal {
    let species: String
    init? (species: String) {
        if species.isEmpty {return nil}
        self.species = species
    }
}

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

if let giraffe = someCreature {
    print("An animal was initialized with a species of \(giraffe.species)")
}

Animal 생성 시 인자를 넣지 않고 생성한 경우 초기화에 실패하여 생성된 인스턴스는 nil을 갖게 되는 것을 확인할 수 있다.

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"

 

- Overriding a Failable Initializer

부모 클래스의 Failable 이니셜라이저를 자식 클래스에서 일반적인 이니셜라이저로 오버라이딩 하는 것이 가능하다.

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

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

6. Required Initializers

모든 자식클래스에서 반드시 구현해야 하는 이니셜라이저는 아래 예제와 같이 required 키워드를 붙여준다.

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

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

 

반응형
Comments