구조체, 클래스, 열거형 이니셜라이저가 실패할 수 있다는 걸 정의하는 것도 필요할 때가 있음!
(초기화 매개변수 값이 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"
이니셜라이저의 매개변수에 따라서 열거형 케이스를 정의한 뒤, 매치되는 케이스가 없을 때 초기화 실패라고 할 수도 있다.
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."
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."
클래스, 구조체, 또는 열거형의 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"
슈퍼클래스의 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]")!
}
}
init? 말고도 init! 으로 failable init을 정의할 수 있삼.
required
키워드를 사용해서 해당 클래스의 서브클래스가 무조건 구현해야 하는 이니셜라이저를 정의할 수가 있다.
class SomeClass {
required init() {
// initializer implementation goes here
}
}
요런 이니셜라이저를 만들고, SomeClass
를 상속하는 서브클래스를 만들면
class SomeSubclass: SomeClass {
required init() {
// subclass implementation of the required initializer goes here
}
}
이렇게 무조건 서브클래스에서 재정의를 해 줘야 하는 이니셜라이저를 만들 수가 있다!