티스토리 뷰

INDEX

1.  필수 생성자(required Initializers)

2.  실패가능 생성자(Failable Initializer)

3.  소멸자(Deinitializer)

4.  마무리

 

 

마지막 이니셜라이저들. 어려운 개념이 아니며, 지정 생성자(Designated Initializers), 편의생성자(Convenience Initializer)가 코어 개념이었기에, 2부때보다는 가볍게 다룬다.

 

(이니셜라이저를 줄여서 생성자로 불렀습니다)

 

 

1.  필수 생성자(required Initializers)

필수생성자(required Initializers)란 class앞에 required 키워드를 붙여 만들며,

필수생성자를 선언한 클래스를 상속받는 서브클래스는 무조건 상위의 필수 생성자를 구현해줘야 한다.

상속을 받을때에는 override키워드 대신에 required키워드를 사용한다⭐️

class Aclass {
    var x: Int
    //필수 생성자
    required init(x: Int) {
        self.x = x
    }
}

class Bclass: Aclass {
//    required init(x: Int) {
//        super.init(x: x)
//    }
}

var me: Bclass = Bclass(x: 2)
me.x //2

// 하위 클래스에서 필수생성자를 구현할 때는, 상위 필수생성자를 구현하더라도
// override(재정의) 키워드가 필요없고, required 키워드만 붙이면 됨

 

필수생성자의 자동상속

필수생성자 역시, 앞 포스팅에서 설명했던 자동상속의 기능이 있다.

서브(하위)클래스에서 새롭게 만든 속성이 초기화 되었고,

별다른 생성자를 정의해주지 않았다면

필수생성자도 자동으로 상속된다.

그렇기에 위의 required init을 주석처리해도 정상작동 하는 것이다.

다만 아래처럼 생성자를 따로 구현한다면?

지정생성자를 만들었기에 자동 상속되지 않는 경우

하위클래스의 지정생성자를 만들고, 상속받은 필수생성자를 그대로 구현하기 (연습용 예제 3)

필수생성자 예제1

class Aclass {
    var x: Int
    //필수 생성자
    required init(x: Int) {
        self.x = x
    }
}

class Cclass: Aclass {
    init() {
        super.init(x: 0)
    }
    
    required init(x: Int) {
        super.init(x: x)       // 호출시 required키워드가 있는 init으로 호출하지 않음(super.init)
    }
}


// init() 생성자를 구현하면, 자동 상속 조건을 벗어나기 때문에
//  필수생성자 required init(x: Int) 무조건 구현해야 함

필수생성자 예제2

class Person{
    var name: String
    
    required init() {
        self.name = "Unknown"
    }
}

class Student: Person{
    var major: String = "Unknown"
  
    //Student의 지정생성자
    init(major: String) {
        self.major = major
        super.init()
    }
    
    //Person 필수생성자 강제 구현
    required init() {
        super.init()
    }
}

class UniversityStudent: Student{
    var grade: String
    
    //UniversityStudent의 지정생성자
    init(grade: String){
        self.grade = grade
        super.init()
    }
    
    required init() {
        self.grade = "A0" //현 단계의 속성 grade초기화
        super.init()
    }
}

let a: Student = Student()
a.major//Unknown
let b: Student = Student(major: "Computer Science")
b.major //Computer Science
let c: UniversityStudent = UniversityStudent(grade: "A+")
c.grade //A+

 

필수 생성자 예제3⭐️(최종단계)

상위클래스의 지정생성자 -> 하위클래스에서 필수생성자로 재정의 
상위의 필수 편의 생성자(required convenience initializer)를 하위에서 똑같이 구현하기

class Person{
    var name: String
    //일반적인 지정생성자
    init() {
        self.name = "Unknown"
    }
}

class Student: Person{
    var major: String = "Unknown"
  
    //Student의 지정생성자
    init(major: String) {
        self.major = major
        super.init()
    }
    
    //Person의 지정생성자를 필수(required)생성자로 재정의
    required override init() {
        super.init()
    }
    
    //이 요구 편의 생성자도 하위클래스에서 계속 요구됨
    required convenience init(name: String){
        self.init() //위에서 정의한 또다른 지정생성자 init()
        self.name = name
    }
}

class UniversityStudent: Student{
    var grade: String
    
    //UniversityStudent의 지정생성자
    init(grade: String){
        self.grade = grade
        super.init()
    }
    //위에서 (기존의 지정생성자엿던걸) 필수생성자로 재정의햇기에 똑같이 구현
    required init() {
        self.grade = "A0" //현 단계의 속성 grade초기화
        super.init()
    }
    //위의 요구 편의생성자 그대로 복붙해서 구현해주면된다
    required convenience init(name: String){
        self.init() //편의생성자기에, 현단계 지정생성자 거치고,
        self.name = name
    }
}

let d:UniversityStudent = UniversityStudent()
d.grade //A0
let t:UniversityStudent = UniversityStudent(name: "tylor")
t.name //tylor

 

필수생성자는 어디에 사용될까?

실제로 필수생성자를 직접 구현할 일은 거의 없기에 이정도 예제만 다루어도 충분하다. 

하지만 Swift에서 실제로 앱을 만들때, 스토리보드로 만들었던게 xml형태로 저장되고,

xml형태로 저장된 파일의 구성을 가져오려면 NSCoding이란 프로토콜이 필요하다.

UIView에 정의된 지정생성자와 필수생성자
프로토콜 NSCoding

UIView나 UIViewController를 상속받아 커스텀뷰를 구성할때

UIView.init(coder:NSCoder)는 반드시 호출된다.

init?(coder:NSCoder)가 없다면 xml형태의 파일을 불러 올 수 없다.

때문에 UIView나 UIViewController를 상속받아 지정생성자를 별도로 구현한다면

아래의 필수 생성자 required init?(coder:  )를 필수 구현해야 한다.

간단히 말하면(지금은 이거만 기억해도 충분. 정확히 알렴녀프로토콜 개념숙지, 앱을 만들어봐야함)

생성자를 따로 정의해주지 않는다면 필수생성자 required init?(coder: )도 자동 상속되겠지만,

UIView를 상속받는 서브클래스에서는 새로운 지정생성자를 정의하기 때문에 required init?구현을 왜 안해주냐는 에러가 발생하는 것이다.

class AView: UIView {
    required init?(coder: NSCoder) {        // error!! 
        fatalError("init(coder:) has not been implemented")
    }
}

NSCoding 프로토콜

NSCoding

 

public protocol NSCoding {
  public func encode(with aCoder: NSCoder)
  public init?(coder aDecoder: NSCoder)
}

실제 사용예시

class BView: UIView {
    
    required init?(coder: NSCoder) {
        //보통은 fatalError를 지우고,
        super.init(coder: coder) //상위 생성자로 명시한뒤
        //아래에 뷰생성에 필요한 코드를 작성한다.
        
        
        
    }
}

 

2.  실패가능 생성자(Failable Initializer) - 클래스만

실패가능 생성자란 인스턴스 생성에 실패할 수도 있는 가능성을 가진 생성자를 말한다.

(앞서 1부의 구조체에서 자세히 설명하였으나 한번 더 정리)

 실패가능 생성자가 만들어진 이유는 옵셔널과 nil을 떠올리면 쉽게 이해할 수 있다.

실패가 불가능하게 만들어서, 아예 에러가 나고 앱이 완전히 꺼지는 가능성보다는

실패가능 가능성 생성자를 정의하고 그에 맞는 예외 처리를 하는 것이 더 올바른 방법이다.

여러가지 이유로 인스턴스를 초기화하는데 실패할 수는 있다.

실패가능 생성자는 생성실패시 nil을 반환해주므로 리턴타입은 옵셔널이다.

따라서 실패가능성을 내포한 생성자를 Failable Initializer라고 부르며 

생성자에 ?를 붙여서 init?(파라미터) 라고 정의하면 실패가능 생성자로 정의된다.

구조체와 열거형은 1부에서 다루었으므로 클래스의 경우만 다룬다.

 

실패가능 생성자 - 상속(Inheritence)관계

클래스는 상속이 있기 때문에, 델리게이트 업(Delegate Up)과 델리게이트 어크로스(Delegate Across)를 떠올려야 함.

 

델리게이트 업(Delegate Up)

상속관계에서의 호출 및 위임( From 하위 To 상위)

(상위)실패가능 생성자 <= (하위) 실패불가능 생성자 (호출/위임) X

(상위)실패불가능 생성자 <= (하위)실패가능 생성자(호출/위임) O

 

델리게이트 어크로스(Delegate Across)

동일관계인 델리게이트 어크로스의 경우

실패가능 생성자 => 실패불가능 생성자 (호출/위임) O

실패불가능 생성자 => 실패가능 생성자(호출/위임) X

 

두 경우 모두 실패 불가능한 생성자에서 실패가능한 생성자는 호출 불가하다.

 

// 상속관계에서의 호출 예제

// 상품

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 }     // 상품의 갯수가 1보다 작으면 ====> 카트 항목 생성 실패
        self.quantity = quantity           // 수량이 한개 이상이면 ====> 초기화 성공
        super.init(name: name)             // "" (빈문자열이면)  ====> 실패 가능 위임 OK
    }
}



if let twoSocks = CartItem(name: "sock", quantity: 2) {
    print("아이템: \(twoSocks.name), 수량: \(twoSocks.quantity)")
}

//아이템: sock, 수량: 2

if let zeroShirts = CartItem(name: "shirt", quantity: 0) {
    print("아이템: \(zeroShirts.name), 수량: \(zeroShirts.quantity)")
} else {
    print("zero shirts를 초기화 불가(갯수가 없음)")
}

//zero shirts를 초기화 불가(갯수가 없음)



if let oneUnnamed = CartItem(name: "", quantity: 1) {
    print("아이템: \(oneUnnamed.name), 수량: \(oneUnnamed.quantity)")
} else {
    print("이름없는 상품 초기화 불가")
}

//이름없는 상품 초기화 불가

상속관계에서의 재정의(Overriding) ( From 상위 To 하위)

(상위)실패가능 생성자 => (하위) 실패불가능 생성자 (호출/위임) O(강제 언래핑) 

(상위)실패불가능 생성자 <= (하위)실패가능 생성자(호출/위임) X 

 

 

// 서류라는 클래스 정의

class Document {
    
    var name: String?
    
    init() {}                // 서류 생성 (실패불가능) (이름은 nil로 초기화)
    
    init?(name: String) {    // 실패가능 생성자 ===> 이름이 "" 빈문자열일때, 초기화 실패(nil)
        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
        }
    }
}


let autoDoc = AutomaticallyNamedDocument(name: "")
autoDoc.name //"[Untitled]"

 

  init! (물음표 대신에 느낌표)

일반적으로는 지금까지처럼 init 키워드 (init?) 뒤에 물음표를 배치하여

적절한 유형의 선택적 인스턴스를 생성하는 실패 가능한 생성자를 정의한다.

하지만 적절한 유형의 암시적으로 래핑되지 않은 선택적 인스턴스를 만드는 실패 가능한 생성자도 정의 가능

-> IUO(암시적 옵셔널 추출) 

 

init? ====> init! 위임 가능

init! ====> init? 위임 가능

init? ====> init! 로 재정의 가능

init! ====> init? 로 재정의 가능

 

3.  소멸자(Deinitializer)

 

소멸자란 메모리에서 해제되기 직전에 정리가 필요한 내용을 구현하는 메서드이며 클래스에만 존재한다.

생성자는 필요한 만큼 구현할 수 있지만, 소멸자 하나만 구현 가능하며 

소멸자는 직접 호출불가, 클래스의 인스턴스(==객체)가 메모리에서 제거되기 직전 자동 호출(이 줄이 제일 중요하다)⭐️

소멸자(초기화 해제) 작동 방식

1. 

Swift는 객체(only class)를 자동 참조 계산하는 ARC 방식을 통해 메모리 관리하므로

일반적인 경우(강한 순환 참조를 제외한)에는 메모리에서 해제될때 수동으로 관리할 필요가 없음

하지만 특별한 작업을 수행중인 경우, 몇가지 추가 정리를 직접 수행해야 할 수 있는데

 ex) 인스턴스에서 파일을 열고 일부 데이터를 쓰는 경우

클래스 인스턴스가 할당 해제되기 전에 파일을 닫아야 파일에 손상이 안 갈 수 있음

 소멸자에서는 인스턴스의 모든 속성에 액세스 할 수 있으며

 해당 속성을 기반으로 동작을 수정할 수 있음 (ex : 닫아야하는 파일 이름 조회)

class Aclass {
    var x = 0
    var y = 0
    
    deinit {
        print("인스턴스의 소멸 시점")
    }
}

var a: Aclass? = Aclass()
a = nil   // 메모리에 있던 a인스턴스가 제거됨

 [상속이 있는 경우]

1. 상위클래스 소멸자는 해당 하위클래스에 의해 상속됨

2. 상위클래스 소멸자는 하위클래스 소멸자의 실행이 끝날 때 자동으로 호출됨

3. 상위클래스 소멸자는 하위클래스가 자체적인 소멸자를 제공하지 않더라도 항상 호출됨

class Aclass {
    var x = 0
    var y = 0
    
    deinit {
        print("A인스턴스의 소멸 시점")
    }
}

class Bclass: Aclass{
    
    deinit{
        print("B인스턴스 소멸 시점")
    }
}

var b: Bclass? = Bclass()
b = nil
//B인스턴스 소멸 시점 (선행)
//A인스턴스의 소멸 시점 (후행)

var a: Aclass? = Aclass()
a = nil   // 메모리에 있던 a인스턴스가 제거됨

 

4.  마무리

이번 포스팅에서는 필수생성자(reqired Initializer)로 시작했다.

 required 키워드로 정의하는 필수생성자는 override키워드 대신 사용하며, 자동상속의 기능도 있었고, 서브클래스에서 편의 생성자로도 재정의가 가능했다.  필수생성자는 UIView를 상속받을때 나타나는 개념이었고, 실제로 만들 상황은 드물다.

 실패가능 생성자(failable Initializer)는 옵셔널타입의 생성자였으며, 실패가능성을 내포한 생성자였고 Delegate up과 Delegate Across, 상속관계의 재정의에서 각각 설명했다. 실패불가능에서 실패가능을 부르는건 불가능했고 실패가능에서 실패 불가능을 부를 수 있었던건 ?타입이기에 nil을 합당하게 가질 수 있어서였다. init!도 있었다만, 옵셔널의 개념을 제대로 이해했다면 쉽게 이해가능하다.

마지막 소멸자(Deinitializer)는 쉬운개념이었고 객체가 메모리에서 제거되기 직전에 호출되는 걸 기억하면 된다.

이걸로 초기화와 생성자에 관한 모든 포스팅을 마치며, 다음은 타입캐스팅

 

 

출처

Inflearn Allen님의 강의 노트

Swift 공식문서

야곰님의 스위프트 프로그래밍 3판

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함