개발자의 삽질
[Swift] Initialization - 3편 (Failable Initializers) 본문
https://docs.swift.org/swift-book/LanguageGuide/Initialization.html
1편 보기 :
2022.01.24 - [Swift] - [Swift] Initialization - 1편
2편 보기
2022.01.27 - [Swift] - [Swift] Initialization - 2편 (Class Inheritance & Initialization)
계속해서 Initialization에 대해 알아보자
Failable Initializers
종종 클래스, 구조체, 열거형의 초기화가 실패할 수 있는 경우를 정의하는 것이 유용 할 때가 있다.
잘못된 변수 값, 필요한 외부 자원이나 다른 조건들로 인해 초기화가 실패하는 경우가 있다.
이러한 경우를 위해 init?
키워드를 사용해 failable initializer 를 사용할 수 있다.
같은 이름과 같은 변수 타입으로 failable, nonfailable 생성자를 만들 수 없다!
failable initializer는 생성시에 옵셔널 값을 생성한다.
failable initializer 안에서는 return nil
이 가능하다.
엄격히 말하자면, 생성자는 값을 반환하지 않는다. 오히려, 생성자의 역할은 초기화가 끝났을 때, self 가 완전하게 초기화되는 것을 보장하는 역할을 담당한다.
초기화 실패를 나타내기 위해 return nil을 하더라도 초기화 성공을 표현하기 위해 return 키워드는 사용하지 않는다.
예를 들어, failable initializer는 숫자 타입 변환에서 사용된다.
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"
아래는 Animal 구조체이다. 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"
Failable Initializers for Enumerations
적절한 열거형 케이스를 선택할 때도 failable 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
}
}
}
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."
Failable Initialzers for Enumerations with Raw Values
원시 값을 갖는 열거형은 자동으로 failable initializer, init?(rawValue:)
를 갖게 된다.
따라서 위의 TemperatureUnit을 아래와 같이 변경할 수 있다.
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."
Propagation of Initialization Failure
클래스, 구조체, 열거형에서의 failable initializer는 같은 클래스, 구조체, 열거형에서 다른 failable initializer로 위임 될 수 있다.
예를들어, 자식 클래스의 failable initializer는 부모 클래스의 failable initializer에게 위임 될 수 있다.
다른 경우로, 만약 다른 생성자를 실패하게 만들게 위임하는 경우, 전체 초기화 과정이 바로 실패하게 되며 더 이상의 초기화 코드는 진행 되지 않게 된다.
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"
Overriding a Failable Initializer
다른 생성자와 같이 부모 클래스의 failable initializer를 자식 클래스에서 override
할 수 있다.
또는, 부모 클래스의 failable initializer를 자식 클래스의 nonfailable initializer로 override
할 수 있다.
이렇게 한다면, 부모 클래스의 초기화 과정은 실패할 수 있어도, 자식 클래스의 생성 과정을 실패 할 수 없게 만들 수 있다.
주의할 점은, 부모 클래스의 failable initializer를 자식 클래스의 nonfailable initializer로 override
할 때, 부모 클래스의 생성자로 위임할 경우 반드시 부모 클래스의 생성 결과에 force-unwrap을 해야 한다.
아래의 Document 클래스는 name value가 nil
익거나, name value가 비어 있지 않은 Document를 생성한다.
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
}
}
}
AutomaticallyNamedDocument는 Document의 두 생성자 모두 override
하고 있다.
AutomaticallyNamedDocument는 초기 값으로 "[Untitled]"를 갖게 되는데, 이는 인스턴스가 이름이 없는 경우와 빈 문자열이 init(name:)
생성자에게 전달될 경우에 해당된다.
AutomaticallyNamedDocument는 부모 클래스의 failable init?(name:)
을 nonfailable init(name:)
생성자로 override
한다. AutomaticallyNamedDocument는 부모클래스와는 빈 문자열을 다르게 접근하기 때문에 실패하지 않고 대신에 실패하지 않는 방법으로 생성자를 제공하고 있다.
또한 forced unwrapping을 이용해서 부모 클래스의 failable initializer를 호출할 수 있다.
class UntitledDocument: Document {
override init() {
super.init(name: "[Untitled]")!
}
}
Required Initializers
required 키워드를 클래스 생성자 앞에 둠으로써 모든 자식 클래스가 반드시 해당 생성자를 구현하게끔 만들 수 있다.
class SomeClass {
required init() {
// initializer implementation goes here
}
}
또한 자식 클래스의 required initializer 구현에도 반드시 required 키워드를 붙여야 한다.
class SomeSubclass: SomeClass {
required init() {
// subclass implementation of the required initializer goes here
}
}
Setting a Default Property Value with a Closure or Function
저장 프로퍼티의 기본 값이 커스터마이징이 필요하거나 초기 세팅이 필요하다면 클로져나 전역 함수를 사용 할 수 있다.
해당 타입의 새 인스턴스가 생성될 때마다, 클로져 또는 함수가 호출되서 반환 값을 저장 프로퍼티의 기본값으로 할당하게 된다.
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
}()
}
클로져 끝에 ()
가 표시되어 있다. 이는 이 클로져를 즉시 실행하라는 뜻이다. 만약 소괄호 표시가 없다면 이는 저장 프로퍼티에 클로져 자체를 할당하게 되는 의미이다.
프로퍼티를 초기화 하기 위해 클로져를 사용해야 할 때 한가지 주의해야 할 것이 있다.
클로져를 실행할 때, 아직 인스턴스화가 끝나지 않았기 때문에 다른 프로퍼티나self
를 사용할 수 없으며 인스턴스 메서드를 사용할 수 없다.
아래는 클로져를 이용한 기본 값 예시이다.
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"
'Swift' 카테고리의 다른 글
[Swift] Swift에서는 어떻게 Optional Protocol을 만들까? (0) | 2022.02.13 |
---|---|
[Swift] Attribute - @discardableResult (0) | 2022.02.09 |
[Swift] Initialization - 2편 (Class Inheritance & Initialization) (0) | 2022.01.27 |
[Swift] Initialization - 1편 (0) | 2022.01.24 |
[Swift] Structure vs Class 무엇을 골라야 할까? (0) | 2022.01.14 |