momodudu.zip

#4 Swift - Initialization 본문

ios/Swift

#4 Swift - Initialization

ally10 2019. 11. 26. 19:38

initialization은 별거 없는줄 알고 그냥 대충 읽고 넘기려고 했는데 ... 읽다보니까 이것저것 복잡한게 많아서 정리해본다.

 

Swift에서 Initialization은 value type, referecne type 둘 다 initialize가 가능하다.

value type의 경우는 상속이 불가능하므로, 그냥 기본 initializer를 생각하면 별로 어렵지 않다.

 

본 글에서는 reference type, 즉 class에서의 초기화에 대해서 적어본다.

기본적으로 class는 반드시 모든 stored property에 대해서 반드시 initialize되어야 한다.

초기화를 하는 방법에는 앞서 몇번 언급했던 것 처럼 default value를 주어서 초기화를 하는 방법이 있고,

Initializer를 선언해서 초기화를 해주는 방법이 있다.

 

1. Initializer 

initializer는 두 가지 종류가 있다. designated initializer와 convenience initializer.

primiary initializer 가 designated initializer 라고 보면 된다. 즉, 모든 stored property가 반드시 초기화 되어야 한다.

 

나중에 설명하겠지만, designated는 상속받은 클래스로 delegate to up이 가능하므로, 이 이니셜라이저에서는

super class의 Initializer 호출이 가능하다.

 

두번째로는 convenience initializer인데, 커스터마이징 한 생성자로 보면 된다.

이건 반드시 구현되어야 하는것은 아니고, 필요한 경우에 맞춰서 구현하면 된다.

 

initializer는 그래서 deleation이 가능한데, deletagation이란 말 그대로 "위임"이라는 뜻이다.

한 인스턴스가 해야하는 일들을 다른 인스턴스로 넘기는 개념이라고 보면 된다.

그래서 이 initializer의 deletagion에는 몇가지 규칙이 있다.

 

- desiganated initializer는 그 super class 의 designated initializer만 부를 수 있다.(=>delegate up)

- convenience initailizer는 같은 class의 다른 Initializer( designated, 혹은 convenience )를 부를 수 있다.(=> delegate across)

 

상속, designated, convenience가 들어가면서 조금 복잡해졌는데 예제를 보면서 이해하면 그렇게 어렵진 않다.

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

 

먼저 Food 라는 Class를 정의한다. name 이라는 stored property를 가지고 있으므로 designated initializer에서 반드시 이를 초기화 해주어야 한다. ( 혹은 default value로 설정해주거나 )

그리고 optional인 convenience initializer도 같이 정의되었다. 이는 커스터마이징 된 초기화로, 만약 인스턴스 생성시 아무런 이름도

파라미터로 넣어주지 않는다면 Default 네임값으로 설정된 name을 designated initalizer로 넘겨준다.

그래서 보면 convenience initializer 에서 self.init(name: ... ) 을 호출해준 것을 볼 수 있다.

 

여기서 위에서 설명한 규칙 두번째가 나타난다.

- convenience initailizer는 같은 class의 다른 Initializer( designated, 혹은 convenience )를 부를 수 있다.(=> delegate across)

 

 

이제 이 클래스를 상속받는 클래스를 하나 더 선언해본다.

class RecipeIngeredient: 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)
        self.init()
    }
}

위에서 선언한 Food Class 를 상속받는 RecipeIngredient Class이다.

Food의 stored property를 상속받을 것이고, Food의 convenience initializer, init(name:String)도 상속받는다.

 

이 클래스의 designated initializer를 보면 상속받은 stored property인 name과 새롭게 선언된 quantity를 초기화해주어야 한다.

단, 해당 예제에서는 이 단계의 클래스에서만 정의된 quantity만 초기화 해주고, 나머지 변수인 name은 super class로 위임한다.(delegate up)

 

convenience initializer는 사실 원하는대로 선택적으로 구현하면 된다.

단, init(name: String) type의 이니셜라이저는 super class에 이미 정의되어 있으므로 override keyword를 붙여서 구현해주어야 한다.

 

class ShoppingListItem: RecipeIngeredient{
    var purchased = false
    
    var description: String{
        var output = "\(name) x \(quantity)"
        output += purchased ? " : Purchased":" : Not purchased"
        
        return output
    }
}

한 단계 더 상속받은 class ShoppingListItem class이다.

디폴트로 purchased 값은 설정되어 있으므로 designated initialzier는 구현하지 않아도 된다.

만약 구현하게 된다면 superClass인 RecipeIngredient의 init(name: String), init(name:String, quantity:Int)와

Food의 init() 을 상속받았기 때문에 호출이 가능하다.

 

단, 위와 같은 형태로 convenience initializer를 구현하고자 하는 경우에는 위에서 했던것처럼 override keyword를 사용해서 재정의 해야 한다.

 

 

2. Failable Initializer

 

Swift 는 initializing이 실패했을 경우에 대한 동작을 정의할 수 있는데, failable initializer라고 한다.

초기화가 실패하는 경우에는 init 에서 받아온 인자들이 초기화할 때 invalid한 인자들이거나, 필요한 외부 source가 초기화 단계에 존재하지 않는 등 여러 경우에 발생할 수 있다.

 

전에 String으로 된 Int, 즉 var myNumber = "123" 을 var number = Int(myNumber) 로 casting할 때 Int Numeric type에서도

failable initializer 가 구현되어 있다..

 

failable initializer의 define은 init? 으로 하면 된다.

// Failable Initializer
class Document{
    var name: String?
    
    // initializer for a document with a nil name value
    init() {}
    
    // failable initializer of superclass
    // initializer for a document with a nonempty name value
    init?(name: String){
        if name.isEmpty {
            return nil
        }
        
        self.name = name
    }
}

 

designated initializer는 init(){} 으로 emtpy body로 선언되어 있다.

class 에선 모든 stored property를 초기화 해주어야 하지만, 이 클래스의 경우 stored property가 String? Type으로 선언되어 있고

이는 defaul로 nil이 assign 되므로 designated initializer는 fully 구현된 상태이다.

 

추가적으로 Failable initializer가 구현되어 있는데, 어떤 name을 인자로 전달받아서 그 Name이 emtpy string인 경우

초기화에 실패했다고 커스터마이징 한 것이다.

 

failable initializer는 Propagation, 즉 fail을 전파할 수 있다.

class Product{
    var name: String
    init?(name: String){
        if name.isEmpty{
            return nil
        }
        
        self.name = name
    }
}

class CartItem: Product{
    var quantity: Int
    init?(name:String, quantity:Int){
        if quantity < 1 {
            return nil
        }
        
        self.quantity = quantity
        super.init(name: name)
    }
}

 

name을 property로 갖는 Proudct class는, 전달받은 인자 name이 empty string인 경우 초기화에 실패가 가능하다. 

그리고 이를 상속받는 CartItem은 quantity가 0이하인 경우에는 초기화 실패가 가능하다.

 

즉,

var myItem = CartItem(name:"Banana", quantity: 0)   // Fail to init CartItem
var myItem2 = CartItem(name:"", quantity: 1)        // Fail to init Product

 

위와 같은 case에서 첫번째는 quantity가 0이므로, CartItem의 failable initializer에서 바로 return nil을 한다.

즉 super class의 생성자는 호출이 되지도 않는다.

 

두번째 케이스에서는 quantity는 존재해서 super class의 init을 호출했지만, emtpy string이기 때문에

super class단에서 nil을 리턴해버려서 결국 초기화는 실패한다. 

 

 

3. Failable initializer의 overriding

subclass에서 failable initializer를 오버라이딩도 가능하다. 대신, super class의 failable initializer는 subclass에서 오버로딩 할 땐

non-failable로 구현되어야 한다. 

 

// Failable Initializer
class Document{
    var name: String?
    
    // initializer for a document with a nil name value
    init() {}
    
    // failable initializer of superclass
    // initializer for a document with a nonempty name value
    init?(name: String){
        if name.isEmpty {
            return nil
        }
        
        self.name = name
    }
}

class AutomaticallyNamedDoc: Document{
    
    // overriding init()
    override init(){
        super.init()
        self.name = "[Untitled]"
    }
    
    // overriding init?(name: String)
    override init(name: String){
        super.init()
        if name.isEmpty{
            self.name = "[Untitled]"
        }
        else{
            self.name = name
        }
    }
}

위 예제의 경우 name이 emptyString이면 fail인 initializer를 subclass인 AutomaticallyNamedDoc class에서 override를 했다.

추가적으로 이 super class의 failable initializer를 오버라이딩 할 시에 super class initializer 호출을 forced unwrapping 형태로도 할 수 있다고 한다.

 

4. Required Initializer

어떤 class에 required initializer를 지정해놓으면, 그 클래스를 상속받는 모든 subclass는 해당 init method를 구현해야 한다.

이는 반드시 오버라이딩 되는 메소드이므로, 따로 method keyword를 쓰지 않는다.

주로 프로토콜간의 상속에서 구현되는 이니셜라이저가 이 required initializer 이다.

 

 

헥헥... 초기화가 이렇게 힘들다니....;

'ios > Swift' 카테고리의 다른 글

#6 Swift - Extension  (0) 2019.11.28
#5 Swift - Optional chaining  (0) 2019.11.27
#3 Swift - Properties  (0) 2019.11.26
#2 Swift - Functions and Closures  (0) 2019.11.25
#1 Swift 기본 문법 - Basics : constant, variables, Optionals  (0) 2019.11.22