Swift/개념 & 응용

[Swift] Failable Initializer (실패 가능한 초기화)

유정주 2023. 8. 8. 13:57
반응형

Failable Initializer

Failable 초기화는 실패가 가능한 초기화입니다.

클래스, 구조체, 열거형에서 실패 가능한 초기화를 정의할 수 있습니다.

 

 

Failable 초기화 예시

실패가 가능한 초기화는 특정 조건에서만 객체가 생성되어야 할 때 유용합니다.

class Time {
    var hour: Int
    var minute: Int
}

예를 들어, Time 클래스는 hour와 minute 변수가 있습니다.

hour의 범위는 1~12여야 하고, minute은 0~59여야 합니다.

이외의 숫자가 들어오면 객체가 생성되지 않도록 하고 싶을 때 Failable 초기화를 사용할 수 있습니다.

class Time {
    var hour: Int
    var minute: Int
    
    init?(hour: Int, minute: Int) {
        guard 1...12 ~= hour, 0...59 ~= minute else {
            return nil
        }
        
        self.hour = hour
        self.minute = minute
    }
}

바로 이렇게요.

 

 

Failable 초기화 정의

Failable 초기화는 init 키워드 뒤에 물음표나 느낌표를 붙여서 정의할 수 있습니다.

초기화 내부에서 조건에 따라 옵셔널 value를 생성합니다.

만약 조건에 맞지 않는 값이 들어온다면 nil 반환해서 초기화를 실패할 있습니다.

 

print(Time(hour: 1, minute: 30)) //OK
print(Time(hour: 13, minute: 30)) //nil

 

실패 범위로 Time 클래스 객체를 생성하면 nil로 반환되는 것을 확인할 수 있습니다.

 

물음표, 느낌표 모두 Failable 초기화라고 부르지만 약간의 동작 차이가 존재합니다.

이에 대해서는 아래에서 다루겠습니다.

 

 

enum의 Failable 초기화

RawValue가 있는 enum의 초기화는 기본적으로 Failable 합니다.

 

enum Color: String {
    case Red, Green, Blue
}

print(Color(rawValue: "Red")) //OK
print(Color(rawValue: "Yellow")) //nil

case에 존재하지 않는 RawValue로 enum 인스턴스를 생성하면 nil이 반환됩니다.

 

물론 직접 init?을 정의할 수도 있습니다.

이럴 경우 switch문을 사용해 직접 실패 조건을 생성할 수 있습니다.

 

 

init!

init?는 성공할 경우 옵셔널 타입 객체를, 실패할 경우 nil을 반환합니다.

하지만 init!는 변수 타입에 따라 성공할 경우 옵셔널을 해제할 수 있습니다.

 

let time1: Time? = Time(minute: -1)
print(time1) //nil

 

위 예제는 minute이 실패 범위이기 때문에 nil이 반환됩니다.

변수 타입이 옵셔널 타입이므로 nil로 받았습니다.

 

하지만 Time 타입으로 받으면 옵셔널 묵시적 해제(Implicitly Unwrapped Optional)가 되서 런타임 에러가 발생합니다.

nil 값이 강제로 해제되서 크래시가 발생하는 것입니다.

 

성공할 경우 편리하게 옵셔널을 벗길 수 있다는 장점이 있지만,

실패할 경우 런타임 에러가 발생하므로 주의하여 사용해야 합니다.

 

 

Failable Initializer 오버로딩

이제 우리가 아는 Swift 초기화는 init, init?, init! 총 세 가지입니다.

세 가지 초기화의 오버로딩과 오버라이딩 관계가 어떻게 되는지 알아봅시다.

 

먼저 오버로딩입니다.

안타깝게도 Failable 여부는 오버로딩이 적용되지 않습니다.

즉, 같은 파라미터를 가지면 Failable이 다를지라도 오버로딩이 적용되지 않습니다.

 

위의 Time 클래스를 예시로 알아봅시다.

이미 init?으로 hour, minute이 존재하기 때문에 컴파일 에러가 발생합니다.

 

당연히 파라미터가 바뀌면 정상적으로 오버로딩 됩니다.

 

 

Failable Initializer 오버라이딩

자식 클래스에서 부모 클래스의 Failable 초기화를 오버라이딩해서 자신만의 실패 조건을 정할 수 있습니다.

이때, 자식 클래스 초기화가 실패하면 즉시 초기화가 종료되서 super.init이 호출되지 않습니다.

 

class ParentA {
    var age: Int
    var name: String

    init?(age: Int, name: String) {
        if age < 0 || name.isEmpty {
            return nil
        }
        self.age = age
        self.name = name
    }
}

class ChildA: ParentA {
    override init?(age: Int, name: String) {
        if age < 10 || name.isEmpty {
            return nil
        }
        super.init(age: age, name: name)
    }
}

ChildA 클래스는 ParentA의 Failable 초기화를 오버라이드 했습니다.

ParentA 클래스의 실패 조건은 age < 0 이고, ChildA 클래스는 실패 조건에 age < 10이 추가되었습니다.

 

이때 자식 클래스에서 super.init을 호출하기 때문에 각 조건이 &&로 적용된다는 점에 주의해야 합니다.

만약 ChildA의 실패 조건이 a < 10이 아니라 a > 0 이라면 a > 0 && a < 0 을 만족해야 초기화가 성공하는거죠.

 

 

nonfailable과 failable 오버라이딩

Failable 초기화 오버라이딩은 nonfailable과 failable 구분이 중요합니다.

 

아래 ParentA와 ChildA 예시로 알아봅시다.

class ParentA {
    var age: Int
    var name: String
    
    init() {
        self.age = 1
        self.name = "unknown"
    }

    init?(age: Int, name: String) {
        if age < 0 || name.isEmpty {
            return nil
        }
        self.age = age
        self.name = name
    }
    
    init!(name: String) {
        if name.isEmpty {
            return nil
        }
        self.age = 1
        self.name = name
    }
}
class ChildA: ParentA {
    // ok
    override init!(age: Int, name: String) {
        if age < 10 || name.isEmpty {
            return nil
        }
        super.init(age: age, name: name)
    }
    
    // ok
    convenience override init(name: String) {
        self.init(age: 10, name: name)
    }

    // compile error
    override init!() {
        
    }
}

 

부모 클래스의 Failable 초기화는 nonfailable, failable 초기화로 오버라이딩 가능하지만,

부모 클래스의 nonfailable 초기화는 nonfailable 초기화로만 오버라이딩 할 수 있습니다.

 

만약 nonfailable을 failable로 오버라이딩하면 위 스크린샷처럼 컴파일 에러가 발생합니다.

 

추가로,

init?을 nonfailable 초기화로 오버라이딩할 경우, super.init 결과를 옵셔널 해제해야 합니다.

init!은 자동으로 옵셔널 묵시적 해제(Implicitly Unwrapped Optional)이 되서 괜찮지만

init?은 결과가 옵셔널이기 때문입니다.

class ParentA {
    var age: Int
    var name: String
	...
    init?(name: String) {
        if name.isEmpty {
            return nil
        }
        self.age = 1
        self.name = name
    }
    ...
}

class ChildA: ParentA {
    ...
    override init(name: String) {
        super.init(name: name)!
        self.age = 10
    }
}

ChildA의 init(name:)은 init?을 nonfailable로 오버라이딩한거라서 결과의 옵셔널을 해제하였습니다.

위에서는 강제 옵셔널 해제를 했는데 옵셔널 바인딩 등으로 처리해도 괜찮습니다.

 

이처럼 Failable 초기화(init?, init!)는 서로 delegate와 오버라이딩 가능하고,

nonfailable, Failable은 서로 delegate 가능합니다. (delegate : init에서 다른 init을 호출하는 것)

단, init에서 init!으로 delegate 할 때 초기화 실패하면 런타임 에러 발생합니다.

(init!의 nil이 옵셔널 해제되서 런타임 에러가 발생)

 

 

이번 포스팅에서는 Failable 초기화, 실패 가능한 초기화에 대해 알아봤습니다.

궁금한 점, 틀린 점은 댓글로 알려주시면 감사하겠습니다!

 

감사합니다!

 

 

참고

https://jeong9216.tistory.com/486#failable%C2%A0initializers

https://babbab2.tistory.com/173

https://ios-development.tistory.com/182

https://beepeach.tistory.com/439

https://wlaxhrl.tistory.com/49


아직은 초보 개발자입니다.

더 효율적인 코드 훈수 환영합니다!

공감 댓글 부탁드립니다.

 

 

반응형