[Swift] init(초기화) (2/2)

·2024년 7월 8일
0

Swift 문법

목록 보기
14/16

1편과 이어집니당

<2편에서 알아볼 것>
클래스에서의 초기화
클래스의 초기화 프로세스
실패 가능한 초기화 구문
필수 초기화 구문


클래스 상속과 초기화

저장 프로퍼티에 초기값을 할당하는 클래스의 2가지 초기화 구문

  • designated initializer(지정 초기화 구문)
  • convenience initializer(편의 초기화 구문)

지정 초기화 구문(designated initializer)

  • 지금까지 써 왔던 init 구문이 바로 지정 초기화 구문.
  • 모든 프로퍼티가 초기화될 수 있도록 한다.
init(<#parameters#>) {
   <#statements#>
}
class Student {
    var name: String
    var age: Int

    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

편의 지정 초기화 구문(convenience initializer)

  • 초기화 구문을 지원하는 보조 초기화 구문
    • 따라서 convenience init을 사용하기 위해서는 init이 구현되어 있어야 함!
  • 초기화 과정을 간편하게 제공하기 위함
    • 따라서 필수는 아니다
  • init의 파라미터를 기본값으로 설정하고, convenience init 내에서 같은 클래스 내에 있는 init을 호출

선언

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이 훨씬 좋겠다는 것을 느낌.



Initializer Delegation - Class Types

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은 깔때기 모양을 의미하는데, 위 그림과 연결시키면 상위 클래스까지 연결하고 이동시킬 수 있는 지점이라고 생각할 수 있을 것 같다.



클래스의 2단계 초기화

Swift에서 클래스 초기화는 총 2단계 프로세스로 동작한다.

첫 번째 단계: 각 저장 프로퍼티가 초기값을 할당받는 과정
두 번째 단계: 인스턴스를 생성하기 전에 클래스의 저장 프로퍼티를 커스텀할 수 있는 단계

첫 번째 단계야 뭐 당연한 과정이니 했는데, 두 번째 단계는… 뭔 말이지?! 했었다 ㅠ.ㅠ

일단은 계속해서 따라가보자.

Swift의 컴파일러는 2단계 초기화가 잘 완료되었는지 판단하기 위해 4가지 검사를 수행한다.

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단계 초기화

이러한 안전 점검을 기반으로 2단계 초기화를 수행함.
앞서 초기화의 1단계는 각 저장 프로퍼티가 초기값을 할당받는 과정이라고 정리했음.

1단계

  1. designated init, convenience init이 클래스에서 호출됨.
  2. 클래스의 새로운 인스턴스에 대한 메모리 할당. 메모리는 아직 초기화는 안 됨
  3. designated init은 모든 저장 프로퍼티가 값을 가지고 있는지 확인하고, 저장 프로퍼티에 대한 메모리는 초기화 됨.
  4. 상위 클래스의 init을 호출하며 상위 클래스로 전달되어 앞선 과정 반복
  5. 최상위 클래스에 도달하여 모든 프로퍼티가 값을 가지고 있다고 확인되면, 인스턴스의 메모리는 완벽하게 초기화되었다고 간주하고 1단계 완료

두 번째 단계: 인스턴스를 생성하기 전에 클래스의 저장 프로퍼티를 커스텀할 수 있는 단계

2단계

  • 체인의 최상위에서 아래로 내려가면, 각 designated init은 인스턴스를 추가로 커스터마이징 가능
    • self로 접근, 프로퍼치 수정, 인스턴스 메서드 호출 등
  • 체인의 모든 convenience init은 인스턴스를 커스터마이징하고 self로 접근할 수 있음.
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 재정의

  • 상위 클래스의 편의 초기화 구문과 일치하는 하위 클래스의 초기화 구문을 작성하는 경우, 상위 클래스의 편의 초기화 구문은 하위 클래스에서 호출될 수 없음
    • 헷갈린다면 Initializer Delegation for Class Types 부분을 다시 읽어보면 좋은데, 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

자동 초기화 구문 상속 (Automatic Initializer Inheritance)

하위 클래스가 기본적으로 상위 클래스의 초기화 구문을 상속하진 않지만, 특정 조건이 충족되어 상위 클래스의 초기화 구문이 상속되는 경우가 있음!

  1. 하위 클래스가 지정 초기화 구문을 정의하지 않으면 자동으로 상위 클래스의 지정 초기화 구문 상속
  2. 하위 클래스가 규칙 1에 따라 상속하거나, 초기화 구문을 재정의하여 상위 클래스의 지정 초기화 구문을 모두 구현한 경우 편의 초기화 구문 상속

예시로 그 경우들을 살펴보자!


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 init
    • 최상위 클래스이므로 당연히 super.init() 필요 없음
  • 파라미터가 없는 convenience 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에 따라 자동으로 상위 클래스에서 모든 초기화 구문들 상속



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

초기화 파라미터 값이 유효하지 않는 등 여러 상황에 대비해 실패 가능한 초기화 구문이 존재.

클래스, 구조체, 열거형 모두에서 정의할 수 있다.

선언

init?
  • 실패하면 nil 을 반환하기 때문에 옵셔널로 생성.

ex) 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.

raw value를 가진 열거형에 대한 실패 가능한 초기화 구문

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.

초기화 실패 전파

  1. 실패 가능한 초기화 구문은 같은 클래스, 구조체, 열거형의 다른 실패 가능한 초기화 구문으로 위임할 수 있음.
  2. 하위 클래스에서 상위 클래스도 가능.

실패 가능한 초기화 구문을 실패 불가능한 초기화 구문에 위임 가능
이는 실패하지 않는 기존 초기화 프로세스가 이미 존재할 때, 실패 상태를 추가해야 하는 경우에 사용할 수 있음

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?과 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
    }()
}

주의) 프로퍼티를 초기화할 때 클로저를 사용하면, 클로저가 실행될 때 인스턴스는 아직 초기화되지 않음.

따라서 다른 프로퍼티의 값에는 접근할 수 없음.



Reference

https://bbiguduk.gitbook.io/swift/language-guide-1/initialization

0개의 댓글

관련 채용 정보