initialization 톺아보기(4) — Failable Initializers, required Initializers

kangYundol
daily-monster
Published in
10 min readJun 1, 2024

지난 글 목록
initialization 톺아보기(1) — 값 타입 초기화
initialization 톺아보기(2) — 클래스 초기화
initialization 톺아보기(3) — 2단계 초기화와 클래스 상속

안녕하세요, 윤돌이입니다. 오늘은 드디어 초기화 마지막 글입니다!!

사실 최근 위젯 개발을 하게 되어서 위젯 개발과 관련된 내용을 포스팅 하고 싶었는데, 생각보다 초기화 내용이 많네요,,ㅎㅎㅎ
드디어 다음주가 되면 위젯 개발기를 포스팅 할 수 있습니다! 우하하

오늘 다룰 내용은 두 가지 입니다.

  1. 실패 가능한 초기화 구문(Failable Initializers)
  2. 필수 초기화 구문(Required Initializers)

실패 가능한 초기화 구문(Failable Initializers)

실패 가능한 초기화 구문은 초기화가 실패할 경우 nil을 반환하는 init 구문입니다.

쉽게 말해 초기화의 옵셔널 버전이라고 생각하면 되어요!

아래 예시는 문자열을 숫자로 변환하는 예시입니다.

이전에 작성한 내용을 바탕으로 생각해보면 원래는 초기화가 되지 않아 컴파일 시점에서 오류가 발생해야 하잖아요?-?

하지만 실패 가능한 초기화 구문을 사용하면 오류가 발생하지 않고 nil을 반환하게 됩니다.

1️⃣ init?

int의 실패 가능한 초기화 구문

여기 보이는 것처럼 실패 가능한 초기화 구문은 뒤에 `?`를 붙여줘야 해요.

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

실패 가능한 초기화 구문은 return nil을 통해 실패 가능 시점을 나타냅니다.

공식 문서의 예제를 보면 species 프로퍼티가 빈 문자열인 경우 초기화가 실패하여 `return nil`을 해주고 있어요.

let someCreature = Animal(species: "Giraffe")

if let giraffe = someCreature {
print("An animal was initialized with a species of \(giraffe.species)")
}

실제 사용을 한다면 이런식으로 사용할 수 있겠죠?

여기서 주의할 점 두 가지가 있습니다!

첫째, 실패 가능한 초기화 구문과 실패 불가능한 초기화 구문은 같은 파라미터와 같은 이름으로 정의할 수 없습니다.

같게 정의한 경우 중복 정의 에러 메세지가 발생합니다.

둘째, init이 반환값을 갖는 것은 아니다!

init에서 return값을 명시해주면

‘nil’ is the only return value permitted in an initializer 라는 오류 메세지를 볼 수 있습니다.

열거형에서의 실패 가능한 초기화 구문

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

열거형에서도 클래스, 구조체와 마찬가지로 실패 가능한 초기화 구문을 사용할 수 있습니다.

여기서 정해진 case가 아닌 경우에는 return nil을 해주고 있어 K, C, F가 아니라면 초기화에 실패한다는 것을 의미합니다.

열거형에서 rawValue를 사용하는 경우

열거형은 기본적으로 init?(rawValue:)라는 초기화 구문을 가지고 있습니다.

이렇게 case에 존재하지 않는 rawValue 값을 사용하는 경우 초기화에 실패하여 nil을 반환해주고 있어요.

그리고 여기서 보면 실패가능한 초기화 구문의 반환값은 옵셔널 타입인 것도 확인할 수 있습니다!

2️⃣ init!

암시적으로 언래핑된 옵셔널 인스턴스를 생성하는 초기화 구문이다.

라고 문서에 나와있는데,, 암시적으로 언래핑된 옵셔널 인스턴스가 뭐지, ,

흠냐,, 직접 init!을 생성해봅시다!

class Test {
var name: String
init?() {
self.name = "hi"
}

init!(name: String) {
if name.isEmpty { return nil }
self.name = name
}
}

let a: Test = Test.init(name: "")
let b: Test = Test.init(name: "윤돌")
let c: Test = Test.init()
print(type(of: a))
print(type(of: b))
print(type(of: c))

여기서 a, b는 init!를 사용하여 만든 인스턴스이고 c는 init?을 활용했습니다. 이때 변수의 타입을 옵셔널이 아니라 Test로 고정해주었어요!

그러면 c에서만 오류가 발생하는 것을 볼 수 있습니다. Test? 타입은 언래핑 되어야 Test가 된다고 해요.

앞에서 실패가능한 초기화를 사용하면 옵셔널 타입으로 반환이 된다고 했는데요, init!을 사용하면 특정 조건에 따라 옵셔널이 아닌 값으로 반환이 됩니다.

만약 타입 지정 없이 init!을 사용하여 인스턴스를 만들면 어떻게 될까요?

타입지정을 해준 경우에는 Test로, 타입지정을 해주지 않은 경우에는 옵셔널 값으로 인스턴스가 생성되는 것을 볼 수 있습니다.

정리하자면 실패 가능한 초기화 중에서도 타입 지정을 해주었을 때 옵셔널이 아닌 인스턴스가 반환되는 초기화 입니다.

3️⃣ 실패 가능한 초기화의 오버라이딩

실패 가능한 초기화 구문 → 실패 불가능한 초기화 구문으로 재정의 가능
실패 불가능한 초기화 구문 → 실패 가능한 초기화 구문으로 재정의 불가!

다시 말하면 init?은 override init으로 재정의 가능하지만, init은 override init?으로 재정의가 불가능합니다.

class Fruits {
var name: String

init() {
self.name = "watermelon"
}

init?(name: String) {
if name.isEmpty { return nil }
self.name = name
}
}

class Apple: Fruits {
override init(name: String) {
super.init()
}
}

let apple = Apple(name: "")
print(apple.name) // "watermelon"

이렇게 빈 문자열이 전달되어도 super.init()을 통해 name이 초기화 될 수 있습니다.

만약 하위 클래스에서 실패 가능한 초기화를 호출하고 싶을 때는 초기화 구문을 강제언래핑 해주면 됩니다!

class Apple: Fruits {
override init(name: String) {
super.init(name: name)!
}
}

필수 초기화 구문(required init)

말 그대로 필수로 정의해야하는 초기화 구문입니다. 필수로 생성해야하는 초기화의 경우 슈퍼 클래스에서 required를 붙이면 서브 클래스에서도 반드시 구현해주어야 하는 것이죠!!

class Fruits {
var name: String

init() {
self.name = "watermelon"
}

required init?(name: String) {
if name.isEmpty { return nil }
self.name = name
}
}

class Apple: Fruits {
required init(name: String) {
super.init(name: name)!
}
}

required 키워드를 사용하면 init에 override 키워드를 붙이지 않아도 됩니다.

+) 추가

여기까지 읽으셨다면 이제 required init?을 이해할 수 있습니다!!

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

Cell과 UIView 등을 만들 때 override init을 생성해주면 항상 요 친구를 필수적으로 적어주어야 한다고 오류가 발생하잖아요?-?

바로 이 NSCoding 때문입니다.

UICollectionViewCell도 UICollectionReusableView를 상속받지만 UICollectionReusableView가 UIView를 상속받기 때문에 마찬가지로 NSCoding을 상속받는다 할 수 있습니다.

NSCoding이 뭔데?

우리가 뷰를 구현하는 방법은 xib나 storyboard를 사용하여 그리는 방법과 코드로 구현하는 방법 두 가지가 존재합니다.

이때 xib와 storyboard 파일은 xml 형태로 저장이 되는데, 이 파일 구성을 가져오는 작업을 init(coder: NSCoder)가 해주고 있어요!

우리가 xib와 storyboard를 사용하는 경우에는 따로 designated init을 생성하지 않으니 자동으로 슈퍼클래스의 required init(coder: NSCoder) 상속받게 됩니다.

하지만 코드로 뷰를 구현하는 경우에는 designated init을 새로 구현하게 되고 이렇게 되면 부모 클래스의 생성자를 상속받지 않게 되기 때문에 따로 required init?(corder: NSCoder)를 명시적으로 생성해줘야 하는 것이죠!!

더 자세한 설명은 여기를 참고해주세요,,

드디어 초기화 끝! 다음에는 위젯 개발로 돌아오겠습니다~!

출처: https://docs.swift.org/swift-book/documentation/the-swift-programming-language/initialization#Failable-Initializers

--

--