티스토리 뷰

Apple 제공 Swift 프로그래밍 가이드(3.0.1)의 Initialization 부분을 공부하며 정리한 글입니다. 개인적인 생각도 조금 들어가있습니다.


이니셜라이저 마지막 편입니다. Failable Initializers, Required Initializers 등을 정리했습니다. 이전 편을 아직 안 읽으셨다면 먼저 읽어봐주세요.

Initialization 1편 링크 > http://wlaxhrl.tistory.com/47
Initialization 2편 링크 > http://wlaxhrl.tistory.com/48



Failable Initializers

클래스, 구조체, ENUM에서 초기화를 하다가 실패하는 경우가 있을 수 있다면 Failable Initializers를 정의하라. 옵셔널 타입처럼 init 뒤에 ? 를 붙이면 된다.

<note> Failable 이니셜라이저와 그렇지 않은 일반 이니셜라이저를 동일한 파라미터 타입/이름으로 둘 다 정의할 수는 없다.

failable 이니셜라이저는 자신이 초기화하는 타입의 옵셔널 값을 생성한다. 초기화 과정 중 실패할지도 모르는 부분에서 return nil을 해주도록 하자.

<note> 초기화가 실패했을 때 return nil을 해야하긴 하지만, 초기화가 성공했을 때도 무언가를 리턴할 필요는 없다. 초기화의 목적은 값 리턴이 아니다.

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

let someCreature = Animal(species: "Rabbit")
if let rabbit = someCreature {
    print("이 동물은 \(rabbit.species)")
}

let anonymousCreature = Animal(species: "")
if let anonymous = anonymousCreature {
    print("이 동물은 \(anonymous.species)")
} else {
    // anonymous는 nil이다
    print("초기화 실패")
}



<ENUM을 위한 Failable Initializer>

ENUM에서 Failable 이니셜라이저 사용하기! 해당하는 케이스가 없는 경우 초기화 실패를 시키고 싶을 때 사용!

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

let fahrenheitUnit = TemperatureUnit(symbol: "F")
if fahrenheitUnit == nil {
    print("초기화 성공해서 여기 안들어온다")
}

let unknownUnit = TemperatureUnit(symbol: "X")
if unknownUnit == nil {
    print("초기화 실패해서 여기에 들어온다")
}


<raw value가 있는 ENUM을 위한 Failable Initializer>

raw value를 사용하는 ENUM은 사실 기본으로 Failable 이니셜라이저가 있었다. init?(rawValue:) 라는 것인데 다음과 같이 활용하면 된다.

enum TemperatureUnit: Character {
    case kelvin = "K", celsius = "C", fahrenheit = "F"
}

let fahrenheitUnit = TemperatureUnit(rawValue: "F")
if fahrenheitUnit == nil {
    print("초기화 성공해서 여기 안들어온다")
}

let unknownUnit = TemperatureUnit(rawValue: "X")
if unknownUnit == nil {
    print("초기화 실패해서 여기에 들어온다")
}



<초기화 실패를 전파하기Propagation of Initialization Failure>

일반 이니셜라이저와 마찬가지로 failable 이니셜라이저도 같은 클래스/구조체/ENUM 내의 다른 failable 이니셜라이저에게 위임delegate할 수 있다. (그 안에서 다른 failable 이니셜라이저를 부른다는 것) 또한 부모클래스의 failable 이니셜라이저에게 위임할 수도 있다. 이 과정에서 실패가 발생하면 (return nil) 곧바로 초기화 과정이 중단된다.

<note> failable 이니셜라이저는 일반 이니셜라이저(nonfailable 이니셜라이저)에게 위임delegate하는 것이 가능하다. 따라서 이미 구현되어 있는 일반 이니셜라이저에서 failable한 상황을 다뤄야 한다면 이 방식을 사용하면 된다. 참고로 그 반대(일반->failable delegate)는 비추를 하고 싶은데... (원래 이니셜라이저의 목적 자체가 완전한 초기화를 보장하는 거니까...) 그래도 해야 할 일이 생긴다면 failable 이니셜라이저를 강제언랩핑 해야한다. 나중에 자세히 다루겠다.

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


// 초기화 성공 케이스
if let twoSocks = CartItem(name: "sock", quantity: 2) {
    print("Item: \(twoSocks.name), 
           quantity: \(twoSocks.quantity)")
}
// Prints "Item: sock, quantity: 2"


// 초기화 실패 - 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"


// 초기화 실패 - 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"



<Failable 이니셜라이저를 오버라이드 하기>

다른 이니셜라이저와 마찬가지로 failable 이니셜라이저도 서브클래스에서 오버라이딩 가능하다.

또 슈퍼클래스의 failable 이니셜라이저를 서브클래스에서는 nonfailable 이니셜라이저(일반 이니셜라이저)로 오버라이딩하는 것도 가능하다.

  • 이 방법으로 슈퍼클래스에서는 초기화 실패가 가능했던 것을 서브클래스에서는 가능하지 않게 만들 수 있다.

  • 그러나 이런 경우 슈퍼클래스의 failable 이니셜라이저로 위임delegate하려면 슈퍼클래스의 failable 이니셜라이저 결과를 강제언랩핑 해야한다.

  • 반대는 안 됨 (슈퍼클래스의 nonfailable -> 서브클래스에서 failable로는 X)

class Document {
    var name: String?
    
    init() {
        // name은 nil
    }
    
    init?(name: String) {
        // name은 빈 문자열이 될 수 없음
        if name.isEmpty {
            return nil
        }
        
        self.name = name
    }
}


class AutomaticallyNamedDocument: Document {
    override init() {
        super.init()
        self.name = "[이름없음]"
    }
    
    // ?가 없다. failable을 non failable로 오버라이드 했음
    override init(name: String) {
        super.init()
        
        // 슈퍼클래스에서 초기화 실패하는 경우를 방지하고는 있다
        if name.isEmpty {
            self.name = "[이름없음]"
        } else {
            self.name = name
        }
    }
}


class UntitledDocument: Document {
    override init() {
        // failable 이니셜라이저를 강제언랩핑하고 있다
        super.init(name: "[이름없음]")!
    }
}



<The init! Failable Initializer>

failable 이니셜라이저를 정의할 때는 보통 ? 마크를 붙여서 init? 으로 정의하지만, 옵셔널 타입이 다 그런 것처럼 이것도 ! 마크를 붙여서 init! 으로 정의할 수 있다. 이 경우 명시적 언랩핑옵셔널이 된다.

init? 에서 init! 으로 delegate 가능, 반대도 가능

init?에서 init! 으로 오버라이드 가능, 반대도 가능

init에서 init! 으로 delegate 가능, 반대도 가능

그러나 init에서 init! 으로 delegate 할 때는 초기화 실패가 발생할 수도 있기 때문에 assertion 에러의 위험이 있다. (nil에 대고 강제언랩핑...)




Required Initializers

클래스의 이니셜라이저 앞에 required 수식어를 붙이면 앞으로 모든 서브클래스는 해당 이니셜라이저를 구현해야 한다. 따라서 서브클래스에서는 굳이 override 키워드를 안붙여도 된다.

class SomeClass {
    required init() {
        
    }
}

class SomeSubClass: SomeClass {
    required init() {
        
    }
}

참고로 서브클래스에서 자동으로 슈퍼클래스의 required 이니셜라이저를 상속받는 경우에는 (저번 포스팅 참고) 굳이 명시적으로 구현할 필요가 없다. (사실 저 예제가 그런 경우다)



클로저/함수로 프로퍼티 디폴트 값 설정하기

stored 프로퍼티의 디폴트 값에 약간의 커스터마이징이 필요할 경우, 클로저나 글로벌 함수를 사용할 수 있다. 그러면 디폴트 값으로 초기화가 이루어질 때마다 해당 클로저나 글로벌 함수가 불리고 그 결과값이 프로퍼티가 초기화된다.

class SomeClass {
    let someProperty: SomeType = {
       // 필요한 작업을 한다
        return someValue
        }()

      // 주의 : 괄호 한 쌍이 붙은 것을 유의깊게 보시오.
      // 괄호가 없다면 클로저를 할당하려는 시도가 된다
}

<note> 위의 클로저가 불리는 시점에서 인스턴스의 나머지 부분은 아직 초기화가 되지 않았을 수도 있다. 디폴트 값을 지닌 프로퍼티라고 해도 말이다. 따라서 클로저 내부에서 다른 프로퍼티에 접근하거나 self 프로퍼티를 쓸 수 없다.

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

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


'Swift 공식 가이드 > Swift 3' 카테고리의 다른 글

Automatic Reference Counting (ARC)  (1) 2017.03.14
Deinitialization  (0) 2017.03.13
Initialization (2/3)  (0) 2017.03.13
Initialization (1/3)  (0) 2017.03.12
Inheritance  (0) 2017.03.01
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함