궁금한 내용을 검색해보세요!
이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
서근 개발노트
티스토리에 팔로잉
SWIFT/Grammar

Swift : 기초문법 [상속#3 - 클래스의 이니셜라이저 convenience, required]

서근
QUOTE THE DAY

-
Written by SeogunSEOGUN

반응형

본 게시글은 yagom님과장국진님의 게시글을 참고하여 작성되었습니다.

 

클래스의 이니셜라이저

값 타입(Struct, Enum)의 이니셜라이저에서는 위임을 위해 이니셜라이저끼리 구분할 필요가 없었지만, Class에서는 지정 이니셜라이저와 편의 이니셜라이저로 역할을 구분한다.

 

즉, 클래스가 부모클래스에서 상속한 모든 속성을 포함하여 모든 클래스의 저장 속성은 초기화하는 동안 초기 값을 지정해야 줘야 한다.

 

Swift에서는 모든 저장 프로퍼티가 기본값을 가지는데 편리하도록 클래스의 유형에 대해 두 가지 이니셜라이저를 정의한다.

지정 이니셜라이저

  • 클래스의 주요 이니셜라이저
  • 클래스의 모든 저장 프로퍼티를 초기화한다
  • 부모클래스의 이니셜라이저를 호출할 수 있다
  • 클래스 내부에는 반드시 한 개 이상의 지정 이니셜라이저가 존재해야 한다
  • 값 타입 이니셜라이저를 정의할 때와 같은 형식인 init 키워드를 사용한다
init(매개 변수들) {
    //초기화 구문
}

편의 이니셜라이저

  • 초기화를 손쉽게 도와주는 역할을 함
  • 지정 이니셜라이저를 자신(편의 이니셜라이저) 내부에서 호출함
  • 지정 이니셜라이저의 일부 매개 변수의 초깃값 설정하여 초기화함
  • 전달 인자로 초깃값을 전달할 필요 없이 항상 같은 값으로 초기화 가능함. 즉, 적은 입력으로 초기화를 편리하게 해 줌
  • 만약 클래스에 편의 이니셜라이저를 쓸 필요가 없다면 사용하지 않아도 된다
  • init 키워드 앞에 convenience 지정자를 명시하면 됨
convenience init(매개 변수들) {
    // 초기화 구문
}
편의 이니셜라이저 단독으로 모든 프로퍼티를 초기화할 수 없다. 일부 프로퍼티만 처리한 뒤 다른 이니셜라이저를 통해 전체 초기화를 수행하고, 일반적으로 중복되는 초기화 코드 방지를 위해 사용한다

사용 방법

class Person {
    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: 27) // 지정 이니셜라이저 호출
    }
}

let seogun: Person = Person(name: "서근")
print("이름: \(seogun.name). 나이: \(seogun.age)") //이름: 서근. 나이: 27

Person 클래스는 nameage 저장 프로퍼티에 대한 초기화가 필요한데, name 값만 입력받고 다른 메서드의 도움을 받아 처리하는 방식을 편의 이니셜라이저이다. 

 

편의 이니셜라이저를 사용하지 않으려면 저장 프로퍼티에 초깃값을 할당해주면 된다.

class Person {
    var name: String
    var age: Int = 27
      
    init(name: String){
        self.name = name
    }
}

let seogun: Person = Person(name: "서근")
print("이름: \(seogun.name). 나이: \(seogun.age)") //이름: 서근. 나이: 27

부모/자식 클래스 관계에서 아래처럼 만약 자식 클래스를 초기화할 때 부모클래스의 super.init()이 먼저 호출되면 isToggle 메서드에서 bright 변수는 초기화되지 않은 상태로 호출되니 주의해야 한다.

class Toggle {
    var on: Bool
    var off: Bool
    
    init(on: Bool, off: Bool) {
        self.on = on
        self.off = off
        isToggle()
    }
    
    func isToggle() {
        print("스위치 상태 on: \(self.on). off: \(self.off)")
    }
}

class Bright: Toggle {
    var bright: Int
    
    init(bright: Int) {
        self.bright = bright
        //super init()이 bright 보다 먼저 호출 되면 bright는 초기화 되지 않음
        super.init(on: true, off: false)
        
    }
    override func isToggle() {
        super.isToggle()
        print("bright: \(self.bright)")
    }
}

let lamp: Bright = Bright(bright: 100)

이니셜라이저 위임 규칙

  • 자식클래스의 지정 이니셜라이저는 부모클래스의 지정 이니셜라이저를 반드시 호출해야 함
  • 편의 이니셜라이저는 반드시 자신을 정의한 클래스의 다른 이니셜라이저를 호출해야 함
  • 편의 이니셜라이저는 지정 이니셜라이저를 호출해야 함

부모클래스는 하나의 지정 이니셜라이저(A)와 두 개의 편의 이니셜라이저(1, 2)가 존재한다. 부모클래스의 편의 이니셜라이저(2)는 다른 편의 이니셜라이저(1)을 호출하고 그 편의 이니셜라이저(1)는 궁극적으로 지정 이니셜라이저(A)를 호출한다. - 조건 2, 3 충족

 

부모클래스는 자신보다 조상인 부모를 갖지 않기 때문에 조건 1 은 해당사항이 없다.

 

자식클래스에는 두 개의 지정 이니셜라이저(B, C)편의 이니셜라이저(3)가 존재한다. 편의 이니셜라이저(3)지정 이니셜라이저(C)를 호출하고, 편의 이니셜라이저는 자신의 클래스에 구현된 아니셜라이저만 호출 가능하므로 부모클래스의 이니셜라이저는 호출 불가하다. - 조건 2, 3 충족

 

또, 두 지정 이니셜라이저(B, C)모두 부모클래스의 지정 이니셜라이저(A)를 호출한다. - 조건 1 충족

 

이제 복잡한 모식도를 보며 지정 이니셜라이저가 어떤 식으로 클래스의 이니셜라이저 중 기둥의 역할을 하는지 알아보면 이해하기 쉬울 겁니다.

2단계 초기화

  1. 자식 및 부모클래스의 모든 저장 프로퍼티에 초깃값을 지정한다
  2. 자식 및 부모클래스의 저장 프로퍼티 값을 변경한다. 그리고 메서드 및 연산 프로퍼티를 사용하여 초기화를 진행한다
class Aclass {
    var name: String
    var age: Int = 26
    
    init(name: String){
        self.name = name
    }
}

class Bclass: Aclass {
    var height: Int
    
    init(height: Int, name: String) {
        //자식 클래스의 속성 값에 값 할당
        self.height = height
        
        //자식클래스의 지정 이니셜라이저는
        //부모클래스의 지정 이니셜라이저를 반드시 호출 해야함
        super.init(name: name)
        
        //부모클래스가 정의한 속성 값 변경
        age = 27
    }
}

let seogun: Bclass = Bclass(height: 200, name: "서근")
seogun.height //200
  • 부모클래스를 호출하기 전, 자식 클래스의 프로퍼티를 초기화해야 한다
  • 부모클래스에 있는 프로퍼티에 따로 값을 할당하기 위해서는 먼저 부모클래스를 호출해야 한다
  • 하지만, 자식 클래스의 프로퍼티가 초기화된 경우에는 자식 클래스의 프로퍼티에 값을 할당하는 구분은 뒤에 동작해도 된다

이니셜라이저 상속

  • Swift는 기본적으로 자식 클래스에서 부모클래스의 이니셜라이저를 상속하지 않는다. (무분별하게 상속되어 자식 클래스를 잘못 초기화하는 상황을 막기 위함)
  • 하지만, 특정 조건을 만족하면 이니셜라이저를 상속함

이니셜라이저 상속을 위한 조건

  • 자식클래스가 지정 이니셜라이저를 정의하지 않은 경우, 지정 이니셜라이저를 상속 받음
  • 자식클래스가 부모클래스의 지정 이니셜라이저를 모두 구현(상속 또는 재정의)한 경우 편의 이니셜라이저를 상속 받음
class Food {
    var name: String
    init(name: String) { // Food 클래스의 지정 이니셜라이저
        self.name = name // Food 클래스의 모든 프로퍼티를 초기화
    }
    convenience init() { // Food 클래스의 편의 이니셜라이저
	// 지정 이니셜라이저를 호출하여 매개 변수에 "[이름 없음]"이란 값 전달
      	self.init(name: "[이름 없음]")
    }
}

let namedMeat = Food(name: "베이컨") // namedMeat의 name은 “베이컨"
let mysteryMeat = Food() // mysteryMeat의 name은 “[이름 없음]"
class RecipeIngredient: Food { // Food를 상속받은 클래스
    var quantity: Int
    // RecipeIngredient 클래스의 지정 이니셜라이저
    init(name: String, quantity: Int) { 
        self.quantity = quantity
        super.init(name: name) // 부모클래스의 이니셜라이저를 호출하여 name에 값 전달
    }
    // 부모클래스의 이니셜라이저와 겹치기 때문에 override 키워드를 
    // 써서 오버라이드 (식별자는 매개변수의 타입과 이름으로 구분하기 때문)
    override convenience init(name: String) { 
        self.init(name: name, quantity: 1)
    }
}
// RecipeIngredient를 상속 받은 클래스
// 새로 추가된 모든 프로퍼티가 기본 값을 가지고 있으며 별도의 
// 이니셜라이저를 적용하지 않았으므로 부모클래스의 모든 이니셜라이저를 자동으로 상속한다.
class ShoppingListItem: RecipeIngredient {
    var purchased = false
    var description: String {
        var output = "\(quantity) x \(name)"
        output += purchased ? " ✔" : " ✘"
        return output
    }
}

var breakfastList = [
    ShoppingListItem(),
    ShoppingListItem(name: "베이컨"),
    ShoppingListItem(name: "달걀", quantity: 6),
]
// 참고: "[이름 없음]"을 "오렌지 쥬스"로 변경 및 구매한 것으로 변경
breakfastList[0].name = "오렌지 쥬스" 
breakfastList[0].purchased = true
for item in breakfastList {
    print(item.description)
}
// 1 x 오렌지 쥬스 ✔
// 1 x 베이컨 ✘
// 6 x 달걀 ✘

전체 코드

<hide/>
class Food {
    var name: String
    init(name: String) { // Food 클래스의 지정 이니셜라이저
        self.name = name // Food 클래스의 모든 프로퍼티를 초기화
    }
    convenience init() { // Food 클래스의 편의 이니셜라이저
	// 지정 이니셜라이저를 호출하여 매개 변수에 "[이름 없음]"이란 값 전달
      	self.init(name: "[이름 없음]")
    }
}

let namedMeat = Food(name: "베이컨") // namedMeat의 name은 “베이컨"
let mysteryMeat = Food() // mysteryMeat의 name은 “[이름 없음]"

class RecipeIngredient: Food { // Food를 상속받은 클래스
    var quantity: Int
    // RecipeIngredient 클래스의 지정 이니셜라이저
    init(name: String, quantity: Int) { 
        self.quantity = quantity
        super.init(name: name) // 부모클래스의 이니셜라이저를 호출하여 name에 값 전달
    }
    // 부모클래스의 이니셜라이저와 겹치기 때문에 override 키워드를 
    // 써서 오버라이드 (식별자는 매개변수의 타입과 이름으로 구분하기 때문)
    override convenience init(name: String) { 
        self.init(name: name, quantity: 1)
    }
}

// 부모클래스의 모든 이니셜라이저를 구현한 경우, 부모클래스가 가진 컨비니언스 이니셜라이저를 상속 받는다.
let oneMysteryItem = RecipeIngredient()
let oneBacon = RecipeIngredient(name: "베이컨")
let sixEggs = RecipeIngredient(name: "달걀", quantity: 6)

// RecipeIngredient를 상속 받은 클래스
// 새로 추가된 모든 프로퍼티가 기본 값을 가지고 있으며 별도의 
// 이니셜라이저를 적용하지 않았으므로 부모클래스의 모든 이니셜라이저를 자동으로 상속한다.
class ShoppingListItem: RecipeIngredient {
    var purchased = false
    var description: String {
        var output = "\(quantity) x \(name)"
        output += purchased ? " ✔" : " ✘"
        return output
    }
}

var breakfastList = [
    ShoppingListItem(),
    ShoppingListItem(name: "베이컨"),
    ShoppingListItem(name: "달걀", quantity: 6),
]
// 참고: "[이름 없음]"을 "오렌지 쥬스"로 변경 및 구매한 것으로 변경
breakfastList[0].name = "오렌지 쥬스" 
breakfastList[0].purchased = true
for item in breakfastList {
    print(item.description)
}
// 1 x 오렌지 쥬스 ✔
// 1 x 베이컨 ✘
// 6 x 달걀 ✘

코드 출처 : 미디엄 장국진

요구 이니셜라이저

  • 모든 자식 클래스에서 반드시 구현해야 하는 이니셜라이저
  • 클래스의 이니셜라이저 앞에 required 수식어를 사용한다. 자식클래스에서 구현할 때도 required 수식어 필수
  • required는 기본적으로 override를 포함한다. (자식클래스에서 요구 이니셜라이저를 재정이 할 때는 override 대신 required 수식어 사용)
class Aclass {
     var name: String
     
     required init() {
     // 이니셜라이저 구현부
     self.name = "서근"
     }
}

//Aclass를 상속받은 SubClass에 요구 이니셜라이저를 구현하지 않았지만,
//SubClass 클래스의 score 프로퍼티에 기본값이 있고 별다른 지정 이니셜라이저가 없기때문에
//이니셜라이저가 자동으로 상속 된것이다.
class SubClass: Aclass {
     var score: Int = 0
}

let seogun: SubClass = SubClass()

Aclass를 상속받은 SubClass에 요구 이니셜라이저를 구현하지 않았지만, SubClass 클래스의 score 프로퍼티에 기본값이 있고 별다른 지정 이니셜라이저가 없기때문에 이니셜라이저가 자동으로 상속된 것이다.

 

만약에 SubClass 클래스에 새로운 지정 이니셜라이저를 구현하면 부모클래스로부터 이니셜라이저가 자동으로 상속되지 않기때문에 요구 이니셜라이저를 반드시 구현해줘야 한다.

class Aclass {
     var name: String
     
     //요구 이니셜라이저 정의
     required init() {
     self.name = "서근"
     }
}

class SubClass: Aclass {
     var score: Int = 0
     
     init(score: Int) {
       self.score = score
       super.init()
     }
     required int() {
       self.score = 50
       super.init()
     }
}

class SecondSubClass {
     var grade: String
     
     init(grade: String) {
        self.grade = grade
        super.init()
     }
     
     required init() {
        self.grade = "A"
        super.init()
     }
}

let seogun: SubClass = SubClass()
print(seogun.score) // 50

let mijin: SubClass = SubClass(score: 90)
print(mijin.score) // 90

let cheolsu: SecondSubClass = SecondSubClass(grade: "B+")
print(cheolsu.grade) // B+

클로저를 활용한 이니셜라이저

  • 기본 값 설정이 단순히 값을 할당하는 것이 아닌 다소 복잡한 계산을 필요로 한다면 클로저나 함수를 이용해 값을 초기화하는데 이용할 수 있다.
  • 클로저가 끝난 뒤에 괄호( ) 를 붙여 속성이 클로저 자체가 아닌 연산의 결과를 가지도록 할 수 있다.
  • 연산 프로퍼티와 다른 점은 연산 프로퍼티는 변수를 참조할 때마다 호출되지만, 클로저를 활용한 이니셜라이저는 한 번만 호출되고, 상수가 아닌 변수로 선언할 경우 값을 변경할 수도 있다.
class SomeClass {
    let someProperty: SomeType = {
        // 이 클로저의 코드로 someProperty를 위한 기본 값을 생성한다.
        // someValue는 SomeType과 반드시 같은 유형이어야 한다.
        return someValue
    }()
}

 

 

읽어주셔서 감사합니다 🤟

 

 

 


잘못된 내용이 있으면 언제든 피드백 부탁드립니다.


서근


위처럼 이미지 와 함께 댓글을 작성할 수 있습니다.