[Swift] Initialization(3)

상 원·2022년 7월 22일
0

Swift

목록 보기
25/31
post-thumbnail

Failable Initializers

구조체, 클래스, 열거형 이니셜라이저가 실패할 수 있다는 걸 정의하는 것도 필요할 때가 있음!
(초기화 매개변수 값이 invalid 하거나, 외부 리소스가 부족하거나, 초기화가 실패할 수 있는 다른 요인이 있는 경우)

요게 실패할 수도 있다는 걸 대응하기 위해서 init? 으로 failable initializer를 정의할 수 있다.

같은 매개변수 타입과 이름으로 failable과 nonfailable 이니셜라이저를 동시에 정의하는 건 불가능함!

이 이니셜라이저는 초기화하는 타입의 옵셔널 타입 값을 생성한다. 실패가 일어날 수 있는 지점에 return nil 을 써주면 됨!

엄밀히 말해서, 이니셜라이저는 값을 리턴해주지 않음.
얘네들은 초기화가 끝나는 시점에 self 가 전체적으로 알맞게 초기화됐는지 확인해주는 역할을 함.
초기화 실패 시 return nil을 쓰긴 하지만, 성공 시 return 을 안 쓰는 데서 알 수 있음!


예를 들어, 숫자 형변환 후 값이 유지된다면 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 doesn't maintain value")
}
// Prints "3.14159 conversion to Int doesn't maintain value"

Int(exactly:) 의 정의를 한번 찾아보면,

/// Creates an integer from the given floating-point value, if it can be
/// represented exactly.
///
/// If the value passed as `source` is not representable exactly, the result
/// is `nil`. In the following example, the constant `x` is successfully
/// created from a value of `21.0`, while the attempt to initialize the
/// constant `y` from `21.5` fails:
///
///     let x = Int(exactly: 21.0)
///     // x == Optional(21)
///     let y = Int(exactly: 21.5)
///     // y == nil
///
/// - Parameter source: A floating-point value to convert to an integer.
public init?(exactly source: Double)

이런식으로 돼 있다.
대충 살펴보면, source로 주어진 숫자를 Int로 변환했을 때 값이 같다면 성공, 값이 다르다면 결과가 nil이 된다는 것!
그래서 위에서 wholeNumber를 Int로 변환했을 때는 값이 같기 때문에 nil이 아닌 변환된 Int값이 valueMaintained에 저장되어 성공문장이 출력된 것이고,
pi를 Int로 변환했을 때는 값이 달라지기 때문에 valueChanged에 nil이 저장되어 실패됐다는 문장이 출력되는 것!!!


요 구조체는 이니셜라이저의 species 로 전달되는 문자열이 비어있으면 초기화 실패, 뭐라도 있으면 성공인 failable initializer를 갖고 있음!

struct Animal {
    let species: String
    init?(species: String) {
        if species.isEmpty { return nil }
        self.species = species
    }
}
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"

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"

Failable Initializers for Enumerations

이니셜라이저의 매개변수에 따라서 열거형 케이스를 정의한 뒤, 매치되는 케이스가 없을 때 초기화 실패라고 할 수도 있다.

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

Remind

Raw Value : 열거형의 케이스에 초기화를 미리 해 둔 값

raw value가 있는 열거형은 자동으로 failable initializer를 받는다.
init?(rawValue:) 인데, 해당 rawvalue를 가진 케이스가 있으면 그걸 선택하고, 없다면 failure를 발생시킴!

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

let fahrenheitUnit = TemperatureUnit(rawValue: "F")
// F를 raw value로 가진 케이스가 있으므로 이걸 선택. nil이 아님.
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")
// X를 raw value로 가진 케이스가 없으므로 nil반환(failure)
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."

Initialization Failure의 전파

클래스, 구조체, 또는 열거형의 failable initializer는 같은 범위의 다른 failable initializer에게 delegate 될 수 있다.
또 서브클래스의 failable initializer는 슈퍼클래스의 failable initializer에게 delegate up 될 수 있다.

둘 다 초기화를 실패하는 이니셜라이저에 delegate하면 초기화 프로세스가 바로 멈추고 초기화 코드도 더 이상 실행되지 않게 됨!

unfailable init에게 delegate할 수도 있음. 이걸 이용해서 이미 존재하는 이니셜라이저에 초기화가 실패할 경우 사용할 수 있는 state를 제공해줄 수 있음!


Product 클래스는 제품의 이름을 저장하는 프로퍼티와 해당 이름이 비어있을 때 호출되는 failable init으로 구성되어 있다.
이것을 상속하는 CartItem 클래스는 장바구니에 담긴 아이템의 갯수를 저장하는 프로퍼티와 해당 아이템의 갯수가 1 밑으로 내려가지 않게 하는 failable init을 가지고 있다. 이 이니셜라이저에서는 super.init(name: name) 으로 Product 의 failable initializer로 delegate up 해서 quantity가 1 이상이며 name이 비어있지 않은 경우에만 초기화가 성공하는 것!

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"

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"

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 init도 서브클래스로 override가 가능함.
심지어 슈퍼클래스의 failable init을 서브클래스의 nonfailable init에 오버라이딩할 수도 있음!!
그러면 슈퍼클래스는 초기화가 실패할 수 있어도 서브클래스는 초기화를 절~~대로 실패하지 않는 고런 이니셜라이저를 만들 수 있다는 것!

다만 이런식으로 오버라이딩할 경우 서브클래스의 이니셜라이저에서 슈퍼클래스의 failable init으로 delegate up 할 수 있는 방법은 슈퍼클래스의 failable init을 강제 언래핑하는 것 뿐임.

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

The init! Failable Initializer

init? 말고도 init! 으로 failable init을 정의할 수 있삼.

Required Initializers

required 키워드를 사용해서 해당 클래스의 서브클래스가 무조건 구현해야 하는 이니셜라이저를 정의할 수가 있다.

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

요런 이니셜라이저를 만들고, SomeClass 를 상속하는 서브클래스를 만들면

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

이렇게 무조건 서브클래스에서 재정의를 해 줘야 하는 이니셜라이저를 만들 수가 있다!

profile
ios developer

0개의 댓글