iOS/Swift

Swift) 초기화(Initializers) 이해하기 (6/6) - Failable Initializers / deinit

소들이 2022. 10. 6. 22:52

 

 

안녕하세요 소들입니다.. zZ

8시 출근 후기

 

 

 

ㅋ.. 

으아앙으아ㅏㅇ졸려 졸려!!! 졸리지만 난 점심시간에 포스팅을 쓸 거야!!!

 

이번 포스팅은 드디어... 이니셜라이저의 막을 내릴 수 있겠네요!!

6편에 걸쳐 진행한 만큼 사실 내용이 조금 어렵기도 하고,

이해하기 어려웠을 수도 있어요...!!!

하지만 이니셜라이저는 기본기 중에서도 정말 중요한 내용이기 때문에..!!

만약 제대로 이해가 안 가시면 몇 번이고 읽어서 완전히 자기 것으로 만드시길 바라겠습니다 :)

 

물론 저도.. 이제 3년꽉한 개발자라서.. 많이 공부하고 쓰지만

잘못된 내용이 있을 수 있으니..! 발견 시 꼭 피드백 주세요!! 😶‍🌫️

모든 포스팅은 편의 말투로 합니다~!!

 

 

 

 

1. Failable Initializers

 

기존 생성자(Nonfailable Initializers)는 컴파일 시점에 모든 프로퍼티가 초기화 되어야 하기 때문에

초기화에 실패할 경우, 컴파일 에러가 발생합니다

하지만 Failable Initializers는 초기화에 실패하더라도 에러가 발생하지 않고 nil을 리턴합니다

생성자의 Optional 버전이라 보면 됩니다

 

사실 생성자의 Optional 버전이라 하면 이해는 가지만,

엥?? 초기화에 실패?? 언제 쓰셈..?? 할 수 있잖음!?

근데 사실 우린 이걸 꽤나 자주 사용하고 있었음!

 

let one: String = "1"
let two: String = "two"
 
let oneNum = Int(one)       // 1
let twoNum = Int(two)       // nil

 

 

이렇게!!!!

우리가 String을 Int로 형변환하고 싶을 때 위처럼 사용했잖음?!?!?

그럼 이때

 

Int(String 타입)

 

이 위 메서드 자체가 바로 Int형이 갖고 있는 생성자 중 하나잖음?

파라미터를 String 타입을 받고 있는!

 

근데 어떻게 "1"을 넣으면, 1이 저장된 Int 자료형을 리턴하고

"two"를 넣으면 숫자로 변환할 수 없으니 nil을 리턴할까!?

생성자가 nil을 어케 리턴하냐 이말이다!! 쾅ㅋ와쾅!!

 

그게 가능한 것이

바로 저 생성자가 우리가 공부하고 있는 failable initializers,

즉 실패가 가능한 초기화이기 때문임 :)

 

실제 생성자의 형태를 보면,

 

 

 

 

이렇게 init 다음에 ?가 붙은 것을 볼 수 있음!!

아까 failable initializers는 이니셜라이저의 Optional 버전이라 했잖음!?

그래서 뒤에 ?가 붙은 것임!

 

이럴 경우, 초기화에 실패할 경우 nil을 리턴할 수 있음!!

실제로, 애플 문서를 보면

 

 

extension Int {
    init?(fromString: String) { 
        if let i = fromString.toInt() {
            // Initialize
            self = i
        } else { 
            // return nil, discarding self is implied
            return nil
        }
    }
}
 

 

 

이렇게 나와 있음!!

Int로 변경할 수 있을 없을 경우엔 nil을 리턴하고 있음

따라서 nil이 리턴된 경우엔 초기화가 실패했음을 의미함!!
(실패할 경우 무조건 nil만 반환해야지, 다른 값으로 대체하면 안됨!!)

 

다만, Optional 생성자라고 말한 만큼!!!

리턴 받은 인스턴스의 타입은 무조건 "옵셔널 타입"임(꼭 기억)

따라서, 우리가 Int(one)으로 인해 생성받은 oneNum에 할당된 인스턴스는

 

 

 

 

이렇게 Int 타입이 아니라 Int? 타입임!!

왜냐? failalbe initializers를 통해 인스턴스를 생성했기 때문!!

 

아 참고로 생성자에서 우리가 상황에 따라 nil을 리턴 시키지만,

생성자에선 리턴 타입을 명시하지 않음!!

 

또 다른 예시로는, 

우리가 enum을 쓸 때 rawValue를 이용해서 쓸 때가 있잖음!?!?

 

 

enum ColorString {
    case Red, Green, Blue
}
 
let RedInstance = Color.init(rawValue: "Red")           // Red
let redInstance = Color.init(rawValue: "red")           // nil

 

 

이렇게 RawValue를 이용해 enum을 초기화 할 때

이때도 RawValue에 해당하는 값이 case로 정의되어 있지 않을 수 있으니,

이때도 failable initializers인 것임!!

따라서, 정의되어 있는 RawValue 중 "Red"가 있을 경우엔 해당 case를 가지는 인스턴스가 반환되고,

정의되어 있는 RawValue 중 "red"가 없을 경우엔 nil이 리턴 됨

 

당연히 RedInstance, redInstance 둘 다 타입은 Color란 열거형의 옵셔널 타입인

Color? 타입이 되는 것임

 

또 생각해봤는데 저번 포스팅에서,

 

 

required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}
 

 

 

이 코드를 공부 했잖음!?!

NSCoder 자체가 xib / storyboard에서 xml 형태의 데이터를 읽어와서 UI를 구성하는 놈이었는데,

만약 해당 xib / storyboard를 어떠한 이유로 읽어올 수 없다면,

그땐 초기화에 실패해야하니까

그래서 init?으로 지정해둔 것이 아닐까 함..! (아님 말구..)

 

 

 

1-1. init?

 

초기화에 성공하면 초기화된 인스턴스가 "Optional Type"으로 리턴

초기화에 실패하면 "nil"을 리턴

 

init?() { ... }
 

 

 

바로 위에서 공부한 것!

 

 

 

1-2. init!

 

초기화에 성공하면 초기화된 인스턴스가 (특정 조건일 때) "Non-Optional Type"으로 리턴

옵셔널 강체 추출이기 때문에, 초기화 실패 시 크래시가 발생

 

이건 IUO랑 관련이 있느데, 만약 IUO가 모른다면 이 포스팅을 먼저 보시구!

어쨌든 옵셔널 강제 추출이란 말로 생각을 해보면,

 

 

class Human {
    let name: String
    
    init?() {
        self.name = "unknown"
    }

    init!(name: String) {
        guard name != "" else { return nil }
        self.name = name
    }
}
 
let sodeul = Human.init(name: "sodeul")   // Human
let deulso = Human.init(name: "")         // nil

 

 

 

자, 봐보자

sodeul란 인스턴스는 init! 생성자의 name을 "sodeul"로 주었으니 정상 초기화 된 건 오케이!

근데 deulso이란 인스턴스는, name을 ""로 주었기 때문에

이는 초기화 실패이고 크래시 발생해야 하는 거 아닌가요?? 이때도 왜 nil이 할당 되죠??

란 생각이 들잖음??

근데 이건 IUO로 생각을 해야할 것 같음 

 

IUO도 Optional Type으로 선언하는 방법 중 하나으로,

Non-Optional Type으로 처리되어야 할 때 값을 자동으로 추출해줌

 

Non-Optional Type으로 처리되어야 할 때 값을 자동으로 추출해주는 것이 IUO잖음??

우리가 IUO 포스팅에서 공부할 때 그 처리되어야 한다는 때가 바로 어떤 때였음!?

바로 IUO로 선언된 놈을 Non-Optional Type에 대입할 때 였음!

 

아까 위에서 초기화에 성공할 경우, (특정 조건일 때) Non-Optional Type의 Instance를 리턴한댔잖음!?

그 특정 조건이 바로 Non-Optional 타입의 인스턴스를 init!으로 초기화할 때를 말한 것임!!

 

자, 정리해보면!

init! 이나 init? 이나 기본적으로 인스턴스를 옵셔널 타입으로 반환하는 것은 똑같음

그러나 init!은 IUO로 동작하기 때문에,

만약 이 생성자를 받는 인스턴스의 타입이 Non-Optional타입일 경우,

Non-Optional Type으로 자동으로 묵시적 옵셔널 바인딩을 해서 리턴해주는 것임

 

따라서,

 

 

 

 

위에서 

Human.init()에서 사용한 생성자는 init? 생성자로,

sodeul의 타입, 즉 옵셔널 타입이 아닌 Human 타입에 대입할 시 초기화 성공/실패와 상관 없이 무조건 위처럼 에러가 남

왜?? Human.init()은 Human? 이란 옵셔널 타입인데! 어떻게 옵셔널 바인딩 없이 넣어!란 컴파일 에러임

 

근데,

Human.init?(name: "deulso")에서 사용한 생성자는 init! 생성자로,

deulso의 타입, 즉 옵셔널이 아닌 Human 타입에 대입할 경우, 에러가 나지 않음

왜?? Human.init?(name:)은 IUO로 인해 자동으로 필요할 때 값이 Non-Optional Type으로 추출되어 들어가는 것임!

 

근데 어라? 아무리 init!이 묵시적 추출이라 해도, 이 아이는 초기화 실패 시 nil을 반환하는데,

어떻게 Non-Optional Type에 대입이 가능한거죠?

 

 

 

 

응 강제추출이야~

만약 실패화에 초기한 인스턴스를 위처럼 강체 추출하게 될 경우,

이땐 컴파일 에러가 아닌 크래시가 남!!

 

당연히 크래시가 날 위험이 있는 코드는 지양하는 게 맞기 때문에

init!은 사실 거의 사용되지 않음ㅎㅎㅎ;;..

 

 

 

1-3. failable initializers의 오버로딩 / 오버라이딩

1️⃣ initializers를 오버로딩 할 경우, failable / nonfailable을 구분하지 않는다

 

 

 

 

이처럼 init?(name: String) , init!(name: String)은 앞에 ?냐 !에 따라 다르니까

오버로딩 가능한 거 아닌가요!? 하겠지만,

initializers의 오버로딩일 경우, failable / nonfailable을 구분하지 않음!!

 

 

 

2️⃣ 슈퍼 클래스의 failable initializers를 자식 클래스에서 nonfailable initializers로 오버라이딩 해도 되지만,

이땐 당연히 failable initializer만 사용할수 없다(다른 놈 쓰던가 / 옵셔널 바인딩 해주든가)

 

 

 

 

위처럼 슈퍼 클래스 Human에서 작성된 init?(name:),

failable initializer서브 클래스 Sodeul에서 오버라이딩 할때,

override init(name:)으로 nonfailable로 오버라이딩 해도 된다는 말임!!!

 

다만, 이때 super.init(name:)은 실패할 수 있는 초기화이기 때문에

이거에대한 옵셔널 해제 작업이 필요함!

 

 

 

음... 내가 생각한 건 nonfailable로 선언된 슈퍼 클래스의 이니셜라이저를 부르는 것이었는데..

super.int(name: name)! 이렇게 슈퍼 클래스의 failable 이니셜라이저를

강제 해제해버리는 방법도 있긴 하답ㄴ디ㅏ...

굳..이요..?....

 

 

 

 

2. deinit

 

소멸자라고 불리며, 클래스 전용으로

인스턴스가 메모리에서 정리되기 전에 자동으로 호출된다

deinit은 메서드 하나만 작성할 수 있게 제한되고, 직접 호출할 수는 없다

 

 

class Human {
    deinit {
        print("☺️")
    }
}
 
var sodeul: Human? = Human.init()
sodeul = nil                            // 메모리에서 해제됨과 동시에 deinit 실행되어 ☺️가 프린트됨
 
 

 

 

요렇게 직접 deinit을 클래스 내에 작성하고,

메모리가 해제되기 전에 해야할 작업을 지정해두면

메모리가 해제될 때 deinit이 불린답니다아 :) 

 

 

 

 

 

 

 

.

.

.

점심시간에 다 작성할 줄 알고 시작한 포스팅이었는데..

꽤나 오래 걸려버렸다..

IUO를 모른다면 쪼금 이해하기 어려웠을 수 있는 포스팅.!

드디어 이니셜라이저가 끝났습니당 :)

잘못된 내용 피드백은 언제나 댓글 주세용!!