1편과 이어집니당
<2편에서 알아볼 것>
클래스에서의 초기화
클래스의 초기화 프로세스
실패 가능한 초기화 구문
필수 초기화 구문
저장 프로퍼티에 초기값을 할당하는 클래스의 2가지 초기화 구문
init
구문이 바로 지정 초기화 구문.init(<#parameters#>) {
<#statements#>
}
class Student {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
선언
convenience init(<#parameters#>) {
<#statements#>
}
ex)
class Student {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
convenience init(name: String) {
self.init(name: name, age: 24)
}
}
let min = Student(name: "Min", age: 25)
let graduate = Student(name: "Mini")
convenience init을 공부했을 때 든 궁금증 💭
init(name: String, age: Int = 24) {
self.name = name
self.age = age
}
convenience init(name: String) {
self.init(name: name, age: 24)
}
init에서 파라미터에 기본값을 설정해 주는 거랑 convenience init의 차이가 뭘까 … 고민했는데 되게 재밌는 예시를 찾았다!
convenience init(name: String) {
var guessedAge: Int
if name == "민준" {
guessedAge = 17
} else if name == "민" {
guessedAge = 25
} else {
guessedAge = 0
}
self.init(name: name, age: guessedAge)
}
조금 말이 안 되는 예시이긴 하지만,,, 💦
이름에 따라 나이를 추측하여 guessedAge
을 구하여 이를 init 호출 시 age
의 값으로 사용하는 예시이다.
이처럼 단순히 기본값을 설정하는 것이 아닌 여러 작업을 한 후에 init을 호출하여 파라미터에 넣을 수도 있다.
이런 경우에는 init의 파라미터에 기본값을 주는 것보다 convenience init이 훨씬 좋겠다는 것을 느낌.
Swift에서는 init과 convenience init 사이의 관계를 단순화 하기 위해 3가지 규칙을 만들어 놓고 적용시킨다고 한다.
규칙 1. 지정된 초기화 구문은 상위 클래스로 부터 지정된 초기화 구문을 호출해야만 합니다.
규칙 2. 편의 초기화 구문은 같은 클래스로 부터 다른 초기화 구문을 호출해야만 합니다.
규칙 3. 편의 초기화 구문은 궁극적으로 지정된 초기화 구문을 호출해야만 합니다.
이를 두 문장으로 요악하면,
지정 초기화 구문은 항상 위로 위임한다.
편의 초기화 구문은 항상 옆으로 위임한다.
<상위 클래스>
convenience init은 항상 옆으로 위임하므로,
하나의 convenience init은 다른 convenience init을 호출.
차례로 convenience init은 designated init을 호출.
<하위 클래스>
convenience init은 마찬가지로 designated init을 호출한다.
이때 하위 클래스의 designated init은 규칙1 충족을 위해 상위 클래스의 designated init을 호출해야 한다.
이처럼 공식 문서에는 designated init을 초기화가 수행되고, 초기화 프로세스가 상위 클래스 체인까지 계속 되는 funnel 지점이라고 표현함.
funnel은 깔때기 모양을 의미하는데, 위 그림과 연결시키면 상위 클래스까지 연결하고 이동시킬 수 있는 지점이라고 생각할 수 있을 것 같다.
Swift에서 클래스 초기화는 총 2단계 프로세스로 동작한다.
첫 번째 단계: 각 저장 프로퍼티가 초기값을 할당받는 과정
두 번째 단계: 인스턴스를 생성하기 전에 클래스의 저장 프로퍼티를 커스텀할 수 있는 단계
첫 번째 단계야 뭐 당연한 과정이니 했는데, 두 번째 단계는… 뭔 말이지?! 했었다 ㅠ.ㅠ
일단은 계속해서 따라가보자.
Swift의 컴파일러는 2단계 초기화가 잘 완료되었는지 판단하기 위해 4가지 검사를 수행한다.
안전 점검1
지정된 초기화 구문은 상위 클래스 초기화 구문에 위임되기 전에, 클래스에 의해 도입된 모든 프로퍼티가 초기화되었는지 확인한다.
class Human {
var name: String
init(name: String) {
self.name = name
}
}
class Student: Human {
var level: Int
// ✅
init(name: String, level: Int) {
self.level = level
super.init(name: name)
}
// ❎ error
// init(name: String, level: Int) {
// super.init(name: name)
// self.level = level
//}
}
규칙 1. 지정된 초기화 구문은 상위 클래스로 부터 지정된 초기화 구문을 호출해야만 합니다.
initializer delegation에서 확인했듯이, 하위 클래스의 init은 상위 클래스의 init을 호출해야만 한다.
이때 하위 클래스의 프로퍼티가 모두 초기화된 후에 상위 클래스의 init에 위임될 수 있다.
안전 점검2
지정된 초기화 구문은 상속된 프로퍼티에 값을 할당하기 전에 상위 클래스 초기화 구문에 위임해야 한다.
상위 클래스의 프로퍼티에 값을 할당하기 전에, 상위 클래스의 init을 먼저 호출해야 한다는 의미이다.
class Student: Human {
var level: Int
// ✅
init(level: Int) {
self.level = level
super.init(name: "ming...")
name = "hmm..."
}
// ❎ error
// init(level: Int) {
// self.level = level
// name = "hmm..."
// super.init(name: "ming...")
// }
}
상위 클래스의 init을 호출하기 전에 상속된 프로퍼티에 값을 할당해 버리면, 상위 클래스의 init이 값을 덮어쓰므로 의미가 없어진다.
안전 점검3
편의 초기화 구문은 모든 프로퍼티에 값을 할당하기 전에 다른 초기화 구문에 위임해야 한다.
class Student: Human {
var nickName: String
var age: Int
init(nickName: String, age: Int) {
self.nickName = nickName
self.age = age
super.init(name: nickName)
}
// ✅
convenience init(age: Int) {
self.init(nickName: "Min", age: age)
self.nickName = "min"
}
// ❎
// convenience init(age: Int) {
// self.nickName = "min"
// self.init(nickName: "Min", age: age)
// }
}
안전 점검2와 마찬가지로 프로퍼티에 값을 할당한 후에 다른 초기화 구문에 위임을 한다면, 어차피 값을 덮어쓰게 되므로 의미가 없어진다.
안전 점검4
초기화 구문은 첫 번째 초기화가 완료될 때까지 인스턴스 메서드를 호출하거나, 인스턴스 프로퍼티의 값을 읽거나,
self
를 값으로 참조할 수 없음
class Student: Human {
var nickName: String
var age: Int
init(nickName: String, age: Int) {
self.nickName = nickName
self.age = age
eat() // self' used in method call 'eat' before 'super.init' call
super.init(name: nickName)
}
func eat() {
}
}
이러한 안전 점검을 기반으로 2단계 초기화를 수행함.
앞서 초기화의 1단계는 각 저장 프로퍼티가 초기값을 할당받는 과정이라고 정리했음.
두 번째 단계: 인스턴스를 생성하기 전에 클래스의 저장 프로퍼티를 커스텀할 수 있는 단계
class Student: Human {
var nickName: String
var age: Int
init(nickName: String, age: Int) {
self.nickName = nickName
self.age = age
super.init(name: nickName)
eat()
name = "test"
}
}
커스터마이징에 대한 의미를 고민해 봤는데, 상위 클래스의 메서드 접근이나 프로퍼티 수정 등에 대한 의미로 이해했다.
이런 과정을 통해서 초기화를 안전하게 수행하고, 프로퍼티 값이 초기화되기 전에 접근하는 것을 막을 수 있음!!!!!!
하위 클래스는 기본적으로 상위 클래스의 초기화 구문을 상속하진 않는다.
단, 상위 클래스의 초기화 구문은 안전하고 적절한 특정 상황에서는 상속되는 경우가 있는데, 이를 자동 초기화 구문 상속이라고 부름. (조금 있음 나올 예정)
designated init 재정의
override
수식어 필요.ex. 기본 초기화 구문
class Student {
var name: String = "Min"
var description: String {
return "Name: \(name)"
}
}
class June: Student {
override init() {
super.init()
name = "June"
}
}
let june: June = June()
june.description // Name: June
저장 프로퍼티에 대해 기본값을 제공하고 사용자 정의 초기화 구문을 작성하지 않았으므로, 기본 초기화 구문 자동 제공됨.
따라서 일치하는 초기화 구문 작성 시에는, override
키워드 필요
if. 하위 클래스의 초기화 구문이 2단계와 같은 커스터마이징이 없고,
상위 클래스가 동기적이며 인수가 없는 초기화 구문을 가진다면,
하위 클래스의 모든 저장 프로퍼티에 값을 할당한 후 super.init()
을 생략할 수 있음
class Student {
var name: String = "Min"
var description: String {
return "Name: \(name)"
}
}
class June: Student {
var age: Int
init(age: Int) {
self.age = age
// super.init()을 암시적으로 호출한다!
}
}
물론 뒤에 name = "june"
이런 식으로 커스터마이징이 필요하면super.init()
호출이 필요하다.
아마 이런 케이스가 많으니 간결하게 코드를 짤 수 있도록 설정해 놓지 않았을까? 싶었다.
convenience init 재정의
override
수식어 사용하지 않음class Student {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
convenience init(name: String) {
self.init(name: name, age: 20)
}
}
class Senior: Student {
var gender: String = "F"
convenience init(name: String) {
self.init(name: name, age: 24)
}
}
let min: Senior = Senior(name: "Min")
min // name: "Min", age: 24
하위 클래스가 기본적으로 상위 클래스의 초기화 구문을 상속하진 않지만, 특정 조건이 충족되어 상위 클래스의 초기화 구문이 상속되는 경우가 있음!
예시로 그 경우들을 살펴보자!
Food
클래스
class Food {
var name: String
init(name: String) {
self.name = name
}
convenience init() {
self.init(name: "[Unnamed]")
}
}
// let namedMeat = Food(name: "Bacon")
name
프로퍼티에 초기값을 설정하는 designated initsuper.init()
필요 없음Food
클래스를 상속하는 RecipeIngredient
클래스를 만듦.
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)
}
}
quantity
프로퍼티에 값을 할당하고 Food
클래스의 designated init으로 위임quantity
프로퍼티에 1을 할당하고 현재 클래스의 designated init으로 위임RecipeIngredient
클래스의 convenience init구문은 Food
클래스의 designated init과 같은 파라미터를 가짐RecipeIngredient
클래스는 지정 초기화 구문을 모두 구현했으므로
규칙2에 따라 자동으로 모든 상위 클래스의 편의 초기화 구문 상속
말이 어렵지만 생각해 보면, 어차피 상위 클래스의 convenience init은 모두 해당 클래스의 designated init을 호출할 것이다.
따라서 하위 클래스가 designated init을 구현했다면, 해당 클래스에서는
super.init(...)
을 통해 상위 클래스의 designated init을 호출할 것이고,따라서 상위 클래스의 convenience init도 사용할 수 있게 되는 것임.
(초기화 구문 위임과 관련된 개념)
RecipeIngredient
를 상속하는 ShoppingListItem
클래스
class ShoppingListItem: RecipeIngredient {
var purchased = false
var description: String {
var output = "\(quantity) x \(name)"
output += purchased ? " ✔" : " ✘"
return output
}
}
하위 클래스의 저장 프로퍼티에 기본값이 있어서 초기화 구문 자체를 정의하지 않았으므로, 규칙 1에 따라 자동으로 상위 클래스에서 모든 초기화 구문들 상속
초기화 파라미터 값이 유효하지 않는 등 여러 상황에 대비해 실패 가능한 초기화 구문이 존재.
클래스, 구조체, 열거형 모두에서 정의할 수 있다.
init?
nil
을 반환하기 때문에 옵셔널로 생성.Animal
구조체struct Animal {
let species: String
init?(species: String) {
if species.isEmpty { return nil }
self.species = species
}
}
species
프로퍼티species
프로퍼티의 값이 빈 문자열이 되면 초기화 실패성공
let someCreature = Animal(species: "Giraffe")
if let giraffe = someCreature {
print("An animal was initialized with a species of \(giraffe.species)")
}
실패
let anonymousCreature = Animal(species: "")
if anonymousCreature == nil {
print("The anonymous creature could not be initialized")
}
// "The anonymous creature could not be initialized"
동물의 species
프로퍼티 값이 빈 문자열인 것은 적절하지 않으므로, 빈 문자열을 찾으면 초기화를 실패하는 실패 가능한 초기화 구문을 구현
ex. 3가지 case가 존재하는 TemperatureUnit
열거형
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 let _ = fahrenheitUnit {
print("initialization succeeded.")
}
// initialization succeeded.
let failureTest = TemperatureUnit(symbol: "Z")
if let _ = failureTest {
print("initialization succeeded.")
} else {
print("initialization failed.")
}
// initialization failed
let failureTest2 = TemperatureUnit(symbol: "I")
if failureTest2 == nil {
print("initialization failed.")
}
// initialization failed.
rawValue
를 활용하여 실패 가능한 초기화 구문 구현 가능
enum TemperatureUnit: Character {
case kelvin = "K", celsius = "C", fahrenheit = "F"
}
let fahrenheitUnit = TemperatureUnit(rawValue: "F")
if fahrenheitUnit != nil {
print("initialization succeed.")
}
// initialization succeed.
let failureTest = TemperatureUnit(rawValue: "W")
if failureTest == nil {
print("initialization failed.")
}
// 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)
}
}
Document
클래스
class Document {
var name: String?
init() {}
init?(name: String) {
if name.isEmpty { return nil }
self.name = name
}
}
비어 있지 않은 문자열이나 nil은 가능하지만
name
프로퍼티가 빈 문자열이면, 초기화가 불가능한 실패 가능한 초기화 구문 정의
Document
클래스를 상속하는 AutomaticallyNamedDocument
클래스
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
}
}
}
초기화 구문 모두 재정의
Document
클래스의 실패 가능한 초기화 구문이 AutomaticallyNamedDocument
에서 실패 불가능한 초기화 구문으로 재정의됨
name
프로퍼티가 빈 문자열이라면 name
프로퍼티는 [Untitled]
가 되도록 보장
상위 클래스의 실패 가능한 초기화 구문을 호출하기 위해 강제 언래핑 사용하기
class UntitledDocument: Document {
override init() {
super.init(name: "[Untitled]")!
}
}
let test = UntitledDocument()
print(test.name) // Optional("[Untitled]")
암시적으로 언래핑된 옵셔널 인스턴스를 생성하는 실패 가능한 초기화 구문도 정의 가능
init! () { }
init?과 init!은 서로 위임과 재정의를 할 수 있음
required
수식어 붙이기required
작성한 초기화 구문을 가진 클래스의 하위 클래스들은 해당 초기화 구문을 구현해야 함기본 init()일 때,
// 상위 클래스
class SomeClass {
required init() {
print("SomeClass init...")
}
}
// 하위 클래스
class SomeSubclass: SomeClass {
required init() {
print("SomeSubclass init...")
}
}
let sb = SomeSubclass()
/*
SomeSubclass init...
SomeClass init...
*/
override
수식어 대신 required
수식어 작성super.init()
이 명시되어 있진 않지만, 상위 클래스의 초기화 구문을 호출근데…
required init()
을 하위 클래스에서 정의해 주지 않아도 만족한다?!
class SomeClass {
required init() {
print("SomeClass init...")
}
}
// 하위 클래스
class SomeSubclass: SomeClass {
}
let sb = SomeSubclass()
// SomeClass init...
required init
은 하위 클래스에서 init을 직접 구현할 경우에만 필수임!
파라미터가 있는 required init(a:)
구문의 경우,
class SomeClass {
required init(param: Int) {
print("Some Calss: initialize paramter", param)
}
}
class AnotherClass: SomeClass {
required init(param: Int) {
print("Another Class: initialize paramter", param)
super.init(param: param)
}
}
let ac = AnotherClass(param: 5)
/*
Another Class: initialize paramter 5
Some Calss: initialize paramter 5
*/
init 구문 작성 시,super.init(param:)
호출이 필요함.
프로퍼티에 클로저나 함수를 사용하여 기본값을 할당할 수 있음
class SomeClass {
let someProperty: SomeType = {
// ...
return someValue
}()
}
주의) 프로퍼티를 초기화할 때 클로저를 사용하면, 클로저가 실행될 때 인스턴스는 아직 초기화되지 않음.
따라서 다른 프로퍼티의 값에는 접근할 수 없음.
https://bbiguduk.gitbook.io/swift/language-guide-1/initialization