大器晩成

실패가능 생성자(Failable Initializers) 본문

iOS/Swift 문법

실패가능 생성자(Failable Initializers)

zerobugpark 2024. 12. 6. 22:38

 

실패가능 생성자(Failable Initializers) - init?(파라미터)

  • 실패가능 생성자는 인스턴스 생성에 실패할 수도 있는 가능성을 가진 생성자입니다. (클래스, 구조체, 열거형 가능)
  • 실패가 불가능하게 만들어서, 아예 에러가 나고 앱이 완전히 꺼지는 가능성보다는 실패가능 가능성 생성자를 정의하고 그에 맞는 예외 처리를 하는 것이 더 올바른 방법입니다.
  • 생성자에 ?를 붙여서 init?(파라미터)라고 정의하면 실패가능 생성자로 정의되는 것입니다.
  • (다만, 동일한 파라미터를 가진 생성자는 유일해야 합니다. 오버로딩으로 실패가능/불가능을 구분 짓지 못함)
  • 실패가능 생성자는 옵셔널 타입입니다.
struct Animal {
    let species: String
    
    // 실패가능 생성자
    init?(species: String) {
        if species.isEmpty {      // isEmpty == "" 빈문자열
            return nil            // 생성자 내에서 실패 가능 부분에 nil을 리턴하면 됨  (문법적 약속)
        }
        self.species = species
    }
    
    // 오버로딩 불가능 (동일한 파라미터를 가진 생성자는 유일해야함)
//    init(species: String) {
//
//    }
    
}


let someCreature = Animal(species: "Giraffe")    // ====> 인스턴스 생성

if let giraffe = someCreature {
    print("초기화된 동물의 종은 \(giraffe.species)")
}


let anonymousCreature = Animal(species: "")           // 문자열이기에 유효한 타입이지만 =====> nil

if anonymousCreature == nil {
    print("익명 크리쳐는 초기활 수 없습니다. ")
}
  • 엄밀히 말하면 초기화 구문은 값을 반환하지 않습니다. 오히려 초기화 구문의 역할은 모든 저장속성이 값을 가져 완전하고 정확하게 초기화되도록 하는 것입니다.
  • 초기화의 실패를 나타내기 위해 return nil을 작성하지만, 초기화 성공을 나타 내기 위해 return 키워드를 사용하지 않습니다 (문법적 약속)

 

열거형의 실패가능 생성자 (Apple 공식문서)

enum TemperatureUnit {
    case kelvin
    case celsius
    case fahrenheit
    
    init?(symbol: Character) {
        switch symbol {
        case "K":
            self = TemperatureUnit.kelvin
        case "C":
            self = TemperatureUnit.celsius
        case "F":
            self = TemperatureUnit.fahrenheit
        default:
            return nil
        }
    }
}

let c: TemperatureUnit = TemperatureUnit.celsius      // TemperatureUnit()
let f: TemperatureUnit? = TemperatureUnit(symbol: "F") // Optional 타입

 

원시값을 가진 열거형에 대한 실패가능 생성자(Apple 공식문서)

// 열거형의 원시값 설정 (실패가능 생성자의 구현과 유사)
enum TemperatureUnit: Character {
    case kelvin = "K"
    case celsius = "C"
    case fahrenheit = "F"
}


// 원시값이있는 열거형은 자동으로 실패가능 생성자 init?(rawValue :)를 구현함 ==> 일치하면 인스턴스 생성, 아니면 nil

 
// 열거형(rawValue: 값)의 경우 열거형의 멤버 리턴
let fahrenheitUnit: TemperatureUnit? = TemperatureUnit(rawValue: "F")     // .fahrenheit
print(fahrenheitUnit!)            // 열거형의 멤버 리턴, fahrenheit
print(fahrenheitUnit?.rawValue)   // 열거형 설정 값 리턴 Optional("F")

let celsiusUnit: TemperatureUnit? = .celsius
print(celsiusUnit!)           // 열거형의 멤버 리턴 celsius
print(celsiusUnit?.rawValue)  // 열거형 설정 값 리턴 Optional("C")

let u: TemperatureUnit? = TemperatureUnit(rawValue: "X")      // nil
  • 열거형의 실패가능 생성자는 원시값을 설정을 통해 유사하게 구현할 수 있습니다.

 

 

초기화 실패의 전달(Propagation of Initialization Failure)

 

 (동일단계 - 델리게이트 어크로스)

  • 실패가능  ===> 실패불가능  (호출/위임) (OK)
  • 실패불가능 ===> 실패가능   (호출/위임) (X) 

 (상속관계 - 델리게이트 업)

  • (하위) 실패불가능  ===> (상위) 실패가능  (호출/위임)  (X)
  • (하위) 실패가능 ===>  (상위) 실패불가능   (호출/위임)  (OK)

 - 두 경우 모두 초기화 실패를 유발하는 다른 생성자에 위임하면

 - 전체 초기화 프로세스가 즉시 실패하고 더 이상 초기화 코드가 실행되지 않음

 

 - (한마디로) 실패불가능 생성자는 다른 실패가능 생성자를 호출 불가능합니다
-> 실패가능이 더 범위가 실패 불가능 보다 포함하는 범위가 크기 때문에, 실패 불가능에서는 실패 가능 생성자를 호출할 수 없습니다.

 

 

동일단계(델리게이트 어크로스)

//실패가능 생성자 -> 실패불가능 생성자 호출 (가능)
struct Item {
    var name = ""
    
    init() {
        
    }
    init?(name: String) {
        self.init()
    }
}

//실패불가능 생성자 -> 살패가능 생성자 호출 (불가능)
struct Item {
    var name = ""
    
    init() {
        self.init(name: "Test")
    }
    init?(name: String) {
        self.name = name
    }
}
  • 실패가능 생성자가 실패불가능 생성자 호출하는 것은 가능합니다.
  • 실패불가능 생성자는 다른  실패가능 생성자 호출하는 것은 불가능합니다.

 

상속관계에서의 호출(델리게이트 업) - Apple 공식문서

// 상속관계에서의 호출 예제

// 상품
class Product {
    let name: String
   
    init?(name: String) {
        if name.isEmpty { return nil }
        self.name = name
    }
    
     //실패가능에서 실패가능 생성자 호출 가능
//    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 }     // 상품의 갯수가 1보다 작으면 ====> 카트 항목 생성 실패
        self.quantity = quantity           // 수량이 한개 이상이면 ====> 초기화 성공
        super.init(name: name)             // "" (빈문자열이면)  ====> 실패 가능 위임 OK
    }
  	
    // 실패불가능 생성자에서 실패가능 생성자 호출은 불가능 
//    init(name: String, quantity: Int) {  
//    self.quantity = quantity          
//    super.init(name: name)            
    }
}



if let twoSocks = CartItem(name: "sock", quantity: 2) {
    print("아이템: \(twoSocks.name), 수량: \(twoSocks.quantity)")
}
// 아이템: sock, 수량: 2


if let zeroShirts = CartItem(name: "shirt", quantity: 0) {
    print("아이템: \(zeroShirts.name), 수량: \(zeroShirts.quantity)")
} else {
    print("zero shirts를 초기화 불가(갯수가 없음)")
}
// zero shirts를 초기화 불가(갯수가 없음)


if let oneUnnamed = CartItem(name: "", quantity: 1) {
    print("아이템: \(oneUnnamed.name), 수량: \(oneUnnamed.quantity)")
} else {
    print("이름없는 상품 초기화 불가")
}
// 이름없는 상품 초기화 불가

 

 

 

상관관계에서 재정의 하기

  • (상위) 실패가능(init?)  ===> (하위) 실패불가능 생성자로  재정의(init) (OK)  (강제 언래핑 활용 가능)
  • (상위) 실패불가능(init) ===> (하위) 실패가능   생성자로 재정의(init?)  (X)
// 서류라는 클래스 정의

class Document {
    
    var name: String?  // 옵셔널이기 때문에 자식에서 부모 클래스를 별도로 초기화 하지 않아도 괜찮음
    
    init() {}                // 서류 생성 (실패불가능) (이름은 nil로 초기화)
    
    init?(name: String) {    // 실패가능 생성자 ===> 이름이 "" 빈문자열일때, 초기화 실패(nil)
        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
        }
    }
}


let autoDoc = AutomaticallyNamedDocument(name: "")
autoDoc.name



// 이름없는(Untitled) 서류
class UntitledDocument: Document {
    
    override init() {               // 재정의 (상위) 실패가능 =====> (하위) 실패불가능
        // 이름을 넣어주기 때문에, 즉, nil이 나올 수 없기 때문에 강제 언래핑이 가능
        super.init(name: "[Untitled]")!    // 강제 언래핑(!)으로 구현 
    }
}
  • 실패가능 생성자의 범위가 더 큼 (범위로 따져보기)

 

실패가능 생성자를 init!(파라미터)로 정의하기

  • 일반적으로 init 키워드 (init?) 뒤에 물음표를 배치하여 적절한 유형의 선택적 인스턴스를 생성하는 실패 가능한 이니셜 라이저를 정의합니다.
  • 적절한 유형의 암시적으로 언래핑된 옵셔널 인스턴스를 생성하는 실패 가능한 이니셜 라이저를 정의할 수 있습니다..

   

 - 물음표 대신 init 키워드 (init!) 뒤에 느낌표

  ================================

 - init? ====> init! 위임 가능

 - init! ====> init? 위임 가능

 

 - init? ====> init! 로 재정의 가능

 - init! ====> init? 로 재정의 가능

 =================================

 

 - init ====> init! 위임 가능 (실패할 수도 있어집니다.)

 

 

 

파라미터가 없는 실패가능 생성자

class Example {
    let isValid: Bool
    
    init?() {
        if Bool.random() {  // 랜덤한 조건
            self.isValid = true
        } else {
            return nil
        }
    }
}

if let instance = Example() {
    print("객체 생성 성공: \(instance)")
} else {
    print("객체 생성 실패")
}
  • 파라미터가 없는 실패 가능 생성자를 만들 수는 있지만, 아무런 조건 없이 실패할 가능성이 없다면, 굳이 실패 가능 생성자로 만들 필요는  없습니다.
  • 위 예제에서도 Bool의 상태를 random으로 받고 있지만, 실제 해당 코드를 사용할 일은 없을 것입니다.
728x90