자식 클래스가 부모 클래스를 상속했다 할지라도 이니셜라이저 역시 자동으로 상속받지 않는다. 자식 클래스에만 존재하는 프로퍼티 디폴트 값을 적용할 때 두 가지 규칙이 있다.
자식 클래스는 부모 클래스의 지정된 이니셜라이저를 자신의 편리한 이니셜라이저로 사용할 수 있다.
부모 및 자식 클래스 간의 상속 관계를 통해 이니셜라이저 사용 방법을 파악해보자.
class Food {
var name: String
init(name: String) {
self.name = name
}
convenience init() {
self.init(name: "[Unnamed]")
}
}
let namedMeat = Food(name: "Bacon")
// namedMeat's name is "Bacon"
let mysteryMeat = Food()
// mysteryMeat's name is "[Unnamed]"
Food
클래스는 이름을 입력받아 인스턴스화하는 지정된 이니셜라이저를 제공하고 있다. 지정된 이니셜라이저는 인스턴스의 모든 저장 프로퍼티가 빈 칸 없이 초기화되도록 보장한다. Food
클래스는 부모 클래스가 없기 때문에 초기화를 완료하기 위해 super.init()
을 호출할 필요가 없다. 또한 편리한 이니셜라이저 init()
을 아규먼트 없이 제공하고 있다.
class RecipeIngredient: Food {
var quantity: Int
init(name: String, quantity: Int) {
self.quantity = quantity
super.init(name: name)
}
override convenience init(name: String) {
self.init(name: name, quantity: 1)
}
}
let oneMysteryItem = RecipeIngredient()
let oneBacon = RecipeIngredient(name: "Bacon")
let sixEggs = RecipeIngredient(name: "Eggs", quantity: 6)
class ShoppingListItem: RecipeIngredient {
var purchased = false
var description: String {
var output = "\(quantity) x \(name)"
output += purchased ? " ✔" : " ✘"
return output
}
}
var breakfastList = [
ShoppingListItem(),
ShoppingListItem(name: "Bacon"),
ShoppingListItem(name: "Eggs", quantity: 6),
]
breakfastList[0].name = "Orange juice"
breakfastList[0].purchased = true
for item in breakfastList {
print(item.description)
}
// 1 x Orange juice ✔
// 1 x Bacon ✘
// 6 x Eggs ✘
Food
를 상속하는 RecipeIngredient
클래스는 이름과 양을 파라미터로 받아 지정된 이니셜라이저 init()
을 사용하고 있다. 이후 init
을 딜리게이트해서 중복 코드 사용을 방지하고 있다.
편리한 이니셜라이저는 이름만 파라미터로 넘겼을 때 호출되는데, 이때 자동으로 양은 1로 설정된다. 이때도 중복 코드를 방지하려고 편리한 이니셜라이저가 지정된 이니셜라이저를 딜리게이트하고 있다. 부모 클래스 Food
의 편리한 이니셜라이저와 다른 이니셜라이저이기 때문에 오버라이딩해서 사용해야 한다.
결과적으로 RecipeIngredient
클래스는 상위 클래스의 모든 지정된 이니셜라이저 및 편리한 이니셜라이저를 상속 등을 통해 제공받는다.
ShoppingListItem
클래스는 RecipeIngredient
클래스를 상속하는 자식 클래스로 이니셜라이저가 없다. 따라서 부모 클래스의 모든 이니셜라이저를 자동으로 상속한다.
breakfastList
는 ShoppingListItem
인스턴스 리터럴 세 개로 구성된 배열이다. 이름 없이 초기화된 인덱스 0 인스턴스는 배열에 들어간 이후 Orange juice
로 입력받는다.
초기화가 실패할 수 있는 이니셜라이저를 정의하기도 한다. 유효하지 않은 초기화 파라미터 값이 들어오거나 필수적인 외부 리소스가 없을 때와 같이 초기화를 방지해야 할 때가 있다.
초기화 실패 상황을 다루기 위해 코드 상으로 ?
키워드를 붙여야 한다. 그런데 같은 종류의 파라미터 타입과 이름을 사용해서 failable
과 nonfailable
이니셜라이저를 동시에 정의할 수는 없다. 즉 따로 선언해야 한다. 초기화가 실패할 때 실패했음을 알려주기 위해 널 값을 리턴하자(indicator
역할).
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"
init(exactly:)
이니셜라이저를 통해 값 변환이 안 될 경우 초기화가 실패한다.
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"
인스턴스 생성 시 초기화 파라미터로 넘겨진 species
가 비었다면 초기화가 실패한다. 이때 널 값과 빈 문자열은 서로 다르다. 빈 문자열은 옵셔널 문자열이 아니기 때문이다.
파라미터가 열거 케이스와 맞지 않으면 초기화가 실패한다.
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."
init?
, 즉 실패 경우를 정하는 이니셜라이에서 파라미터로 입력받은 symbol
이 열거 케이스와 맞지 않으면 널을 리턴, 즉 실패하고 있다.
init?(rawValue:)
로 바로 열거 케이스를 찾을 수도 있다.
enum TemperatureUnit: Character {
case kelvin = "K", celsius = "C", fahrenheit = "F"
}
let fahrenheitUnit = TemperatureUnit(rawValue: "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(rawValue: "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."
초기화 실패 시 같은 타입(클래스, 구조체, 열거형)의 다른 실패 가능한 이니셜라이저로 딜리게이트할 수 있다. 자식 클래스에서 부모 클래스로도 딜리게이트되기 때문에 중복 코드를 줄일 수 있다. 즉 초기화가 실패한다면 전 과정이 실패하기 때문에 컴파일 시간도 줄일 수 있다.
실패 가능한 이니셜라이저는 실패하지 않는 이니셜라이저로 딜리게이트될 수도 있다.
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 you try to create a CartItem instance with a quantity value of 0, the CartItem initializer causes initialization to fail:
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"
파라미터로 입력받은 quantity
가 1 이상이면 유효하지만, 그렇지 않으면 초기화 과정에서 실패하도록 작성되어 있다. name
도 빈 문자열이면 실패한다.
실패 가능한 이니셜라이저도 오버라이딩할 수 있다. 부모 클래스의 실패 가능한 이니셜라이저를 자식 클래스가 nonfailable
이니셜라이저로 오버라이딩할 수도 있다. 이때 부모 클래스 이니셜라이저를 딜리게이트하려면 부모 클래스의 실패 가능한 이니셜라이저 결괏값을 강제로 언래핑해야 한다. 이때 nonfailable
을 failable
로 오버라이딩할 수는 없다.
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]")!
}
}
Document
클래스를 상속하는 AutomaticallyNamedDocument
자식 클래스는 init?
을 상속하지만 오버라이딩을 통해 널 값 또는 허용하지 않은 케이스의 경우를 조건문을 통해 제어 가능. 이때 강제 언래핑이 사용되기도 한다.
init!
, failable
이니셜라이저옵셔널 인스턴스를 허용 하는 실패 가능한 이니셜라이저를 정의할 수도 있다. 부모/자식 클래스 관계에서 init?
에서 init!
을 딜리게이트하거나 오버라이딩할 수 있다. init!
은 옵셔널을 허용한 이니셜라이저를 암시적으로 언래핑한 이니셜라이저라고 생각하자.
이니셜라이저 앞에 required
키워드를 통해 이 클래스를 상속하는 모든 자식 클래스가 이 이니셜라이저를 사용해야 함을 표시할 수 있다.
class SomeClass {
required init() {
// initializer implementation goes here
}
}
class SomeSubclass: SomeClass {
required init() {
// subclass implementation of the required initializer goes here
}
}
필수적인 지정된 이니셜라이저를 오버라이딩할 때에는 오버라이딩 키워드를 쓰지 않는다.
저장 프로퍼티 디폴트 값에 커스터마이징이나 설정이 필요하면 클로저나 전역 함수를 사용할 수 있다. 프로퍼티가 속하는 타입의 새로운 인스턴스가 초기화되면 클로저나 함수가 호출되고 리턴 값이 프로퍼티 디폴트 값으로 할당된다. 이때 클로저나 함수가 프로퍼티와 같은 타입의 임시 값을 생성하고 프로퍼티 디폴트 값으로 리턴한다.
class SomeClass {
let someProperty: SomeType = {
// create a default value for someProperty inside this closure
// someValue must be of the same type as SomeType
return someValue
}()
}
클로저 문 마지막 ()
을 통해 클로저를 즉시 종료할 수 있다. 괄호가 없으면 클로저가 프로퍼티에 할당되버린다.
프로퍼티 초기화로 클로저를 사용할 때 클로저가 실행되는 시점에서는 아직 인스턴스 초기화가 완료되지 않는다는 점도 기억하자. 즉 클로저 내부에서 프로퍼티 값에 접근할 수 없다는 말이다. 셀프 프로퍼티나 인스턴스 메소드도 사용 불가다.
struct Chessboard {
let boardColors: [Bool] = {
var temporaryBoard: [Bool] = []
var isBlack = false
for i in 1...8 {
for j in 1...8 {
temporaryBoard.append(isBlack)
isBlack = !isBlack
}
isBlack = !isBlack
}
return temporaryBoard
}()
func squareIsBlackAt(row: Int, column: Int) -> Bool {
return boardColors[(row * 8) + column]
}
}
let board = Chessboard()
print(board.squareIsBlackAt(row: 0, column: 1))
// Prints "true"
print(board.squareIsBlackAt(row: 7, column: 7))
// Prints "false"
인스턴스를 생성할 때마다 클로저가 실행되고 디폴트 값이 리턴된다. 앞에서 말한 것처럼 클로저는 이때 임시 배열을 사용해 연산한다.