상속 2 (클래스의 이니셜라이저 상속과 재정의)

Gooreum·2021년 11월 14일
0

Swift

목록 보기
16/16

클래스의 이니셜라이저 상속과 재정의

  • 참조 타입인 클래스의 이니셜라이저 위임은 지정 이니셜라이저편의 이니셜라이저로 역할을 구분한다.
  • 값 타입의 이니셜라이저는 상속을 고려할 필요가 없지만 클래스는 상속이 가능하므로 상속받았을 때 이니셜라이저를 어떻게 재정의하는지에 대한 규칙을 알아야 한다.

지정 이니셜라이저와 편의 이니셜라이저

지정 이니셜라이저(Designated Initializer)

  • 지정 이니셜라이저는 클래스의 주요 이니셜라이저로써 필요에 따라 부모클래스의 이니셜라이저를 호출할 수 있으며, 이니셜라이저가 정의된 클래스의 모든 프로퍼티를 초기화해야 하는 임무를 가지고 있다.
  • 지정 이니셜라이저는 클래스의 이니셜라이저 중 기둥과 같은 역할을 하므로 클래스에 하나 이상 정의한다.
    • 편의 이니셜라이저에 비하면 적은 수이다.
  • 만약 조상클래스에서 지정 이니셜라이저가 자손클래스의 지정 이니셜라이저 역할을 충분히 할 수 있다면, 자손클래스는 지정 이니셜라이저를 갖지 않을 수도 있다.
    • 아마도 이런 경우는 조상클래스로부터 물려받은 프로퍼티를 제외하고 옵셔널 저장 프로퍼티 외에 다른 저장 프로퍼티가 없을 가능성이 크다.

편의 이니셜라이저(Convenience Initializer)

  • 편의 이니셜라이저는 초기화를 좀 더 손쉽게 도와주는 역할을 한다.
  • 편의 이니셜라이저는 지정 이니셜라이저자신 내부에서 호출한다.
  • 지정 이니셜라이저의 매개변수가 많아 외부에서 일일이 전달인자를 전달하기 어렵거나 특정 목적에 사용하기 위해서 편의 이니셜라이저를 설계할 수도 있다.
  • 지정 이니셜라이저를 사용하면 인스턴스를 생성할 때마다 전달인자로 초깃값을 전달해야 하지만 편의 이니셜라이저를 사용하면 항상 같은 값으로 초기화가 가능하다.
  • 편의 이니셜라이저는 필수 요소는 아니지만, 클래스 설계자의 의도대로 외부에서 사용하길 원하거나 인스턴스 생성 코드를 작성하는 수고를 덜 때 유용하게 사용가능 하다.
[지정 이니셜라이저]
init(매개변수들) { 
	초기화 구문
}

[편의 이니셜라이저]
convenience init(매개변수들) { 
	초기화 구문
}

클래스의 초기화 위임

  • 지정 이니셜라이저와 편의 이니셜라이저 관계
    1. 자식클래스의 지정 이니셜라이저는 부모클래스의 지정 이니셜라이저를 반드시 호출해야 한다.
    2. 편의 이니셜라이저는 자신을 정의한 클래스의 다른 이니셜라이저를 반드시 호출해야 한다.
    3. 편의 이니셜라이저는 궁극적으로는 지정 이니셜라이저를 반드시 호출해야 한다.

2단계 초기화

스위프트의 클래스 초기화는 2단계(two-phase)를 거친다.

1단계

  • 1단계 초기화는 클래스에 정의한 각각의 저장 프로퍼티에 초깃값이 할당된다.
  • 모든 저장 프로퍼티의 초기 상태가 결정되면 2단계로 돌입해 저장 프로퍼티들을 사용자 정의할 기회를 얻는다.
  • 그 후 비로소 새로운 인스턴스를 사용 할 준비가 끝난다.

2단계

  • 2단계 초기화는 프로퍼티를 초기화하기 전에 프로퍼티 값에 접근하는 것을 막아 초기화를 안전하게 할 수 있도록 해준다.

  • 다른 이니셜라이저가 프로퍼티의 값을 실수로 변경하는 것을 방지할 수도 있다.

  • 스위프트 컴파일러는 2단계 초기화를 오류 없이 처리하기 위해 다음과 같은 네 가지 안전확인(Safty-Checks)을 실행한다.

    1. 자식클래스지정 이니셜라이저가 부모클래스의 이니셜라이저를 호출하기 전에 자신의 프로퍼티를 모두 초기화했는지 확인한다.
      a. 그래서 자식클래스의 지정 이니셜라이저에서 부모클래스의 이니셜라이저를 호출하기 전에 자신의 모든 (기본값이 없는) 저장 프로퍼티에 값을 할당해주어야 한다.
    2. 자식클래스지정 이니셜라이저상속받은 프로퍼티에 값을 할당하기 전에 반드시 부모클래스의 이니셜라이저를 호출해야 한다.
    3. 편의 이니셜라이저는 자신의 클래스에 정의한 프로퍼티를 포함하여 그 어떤 프로퍼티라도 값을 할당하기 전에 다른 이니셜라이저를 호출해야 한다.
    4. 초기화 1단계를 마치기 전까지는 이니셜라이저는 인스턴스 메서드를 호출할 수 없다. 또 인스턴스 프로퍼티의 값을 읽어들일 수도 없다. self 프로퍼티를 자신의 인스턴스를 나타내는 값으로 활용할 수도 없다.
  • 위의 네 가지 안전확인에 근거하여 어떻게 2단계 초기화가 이루어지는지 살펴보도록 한다.

    1단계

    1. 클래스가 지정 또는 편의 이니셜라이저를 호출한다.

    2. 그 클래스의 새로운 인스턴스를 위한 메모리가 할당된다. 메모리는 아직 초기화되지 않은 상태이다.

    3. 지정 이니셜라이저는 클래스에 정의된 모든 저장 프로퍼티에 값이 있는지 확인한다. 현재 클래스 부분까지의 저장 프로퍼티를 위한 메모리는 이제 초기화되었다.

    4. 지정 이니셜라이저는 부모클래스의 이니셜라이저가 같은 동작을 행할 수 있도록 초기화를 양도한다.

    5. 부모클래스는 상속 체인을 따라 최상위 클래스에 도달할 때까지 이 작업을 반복한다.

      2단계

      1.최상위 클래스로부터 최하위 클래스까지 상속 체인을 따라 내려오면서 지정 이니셜라이저들이 인스턴스를 제 각각 사용자 정의하게 된다. 이 단계에서는 self를 통해 프로퍼티 값을 수정할 수 있고, 인스턴스 메서드를 호출하는 등의 작업을 진행할 수 있다.
      2.마지막으로 각각의 편의 이니셜라이저를 통해 self를 통한 사용자 정의 작업을 진행할 수 있다.

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

class Student: Person {
    var major: String = "F"
    
    init(name: String, age: Int, major: String) {
        self.major = major
        super.init(name: name, age: age)
    }
    
    convenience init(name: String) {
        self.init(name: name, age: 4, major: "")
    }
		func test() { print("test") }
}
  • Student 클래스의 지정 이니셜라이저는 부모클래스의 지정 이니셜라이저를 호출하기 전에 자신의 self 프로퍼티를 이용해 major 프로퍼티의 값을 할당한다. → 안전확인 중 1번의 조건을 만족한다.
  • super.init(name: name, age: age) 를 통해 부모클래스의 이니셜라이저를 호출했으며 그 외에 상속받은 프로퍼티가 없으므로 부모의 이니셜라이저 호출 이후에 값을 할당해줄 프로퍼티가 없다. → 안전확인 중 2번의 조건을 만족한다.
  • 편의 이니셜라이저인 convenience init(name:)은 따로 차후에 값을 할당할 프로퍼티가 없고, 다른 이니셜라이저를 호출했다. → 안전확인 중 3번의 조건을 만족한다.
  • 이니셜라이저 어디에서도 인스턴스 메서드를 호출하거나 인스턴스 프로퍼티의 값을 읽어오지 않는다.
    • super.init(name: name, age: age) 아래에 self.test() 메서드를 호출하면 호출 가능해진다.

      → 안전확인 중 4번의 조건을 만족한다.

이니셜라이저 상속 및 재정의

  • 보통 부모클래스의 이니셜라이저와 똑같은 이니셜라이저를 자식클래스에서 사용하고 싶다면 자식클래스에서 부모의 이니셜라이저와 똑같은 이니셜라이저를 구현해주면 된다.
    • 기본적으로 스위프트의 이니셜라이저는 부모클래스의 이니셜라이저를 상속받지 않는다. 부모클래스로부터 물려받은 이니셜라이저는 자식클래스에 최적화되지 않기 때문이다.
  • 부모클래스와 동일한 지정 이니셜라이저를 자식클래스에서 구현해주려면 재정의하면 된다. 그러려면 override 수식어를 붙여야 한다. 자식클래스의 편의 이니셜라이저가 부모클래스의 지정 이니셜라이저를 재정의하는 경우에도 override 수식어를 붙여주면 된다.
  • 반대로 부모클래스의 편의 이니셜라이저와 동일한 이니셜라이저를 자식클래스에 구현할 때는 override 수식어를 붙이지 않는다. 자식클래스에서 부모클래스의 편의 이니셜라이저는 절대로 호출할 수 없기 때문이다. 즉, 재정의할 필요가 없다.
class Person {
    var name: String = ""
    var age: Int = 0
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
    
    convenience init(name: String) {
        self.init(name: name, age: 0)
    }
}

class Student: Person {
    var major: String = "F"
    
    override init(name: String, age: Int) {
        self.major = "Swift"
        super.init(name: name, age: age)
    }
    
    convenience init(name: String) {
        self.init(name: name, age: 4)
    }
}
  • 부모클래스의 실패 가능한 이니셜라이저를 자식클래스에서 재정의하고 싶을 때는 실패 가능한 이니셜라이저로 재정의해도 되고 필요에 따라서 실패하지 않는 이니셜라이저로 재정의해줄 수도 있다.

이니셜라이저 자동 상속

  • 기본적으로 스위프트의 이니셜라이저는 부모클래스의 이니셜라이저를 상속받지 않지만 특정 조건에 부합한다면 부모클래스의 이니셜라이저가 자동으로 상속된다.
  • 자식클래스에서 프로퍼티 기본값을 모두 제공한다고 가정할 때, 다음 두 가지 규칙에 따라 이니셜라이저가 자동으로 상속된다. **규칙1** : 자식클래스에서 별도의 지정 이니셜라이저를 구현하지 않는다면, 부모클래스의 지정 이니셜라이저가 자동으로 상속된다. **규칙2** : 만약 규칙1에 따라 자식클래스에서 부모클래스의 지정 이니셜라이저를 자동으로 상속받은 경우 또는 부모클래스의 지정 이니셜라이저를 모두 재정의하여 부모클래스와 동일한 지정 이니셜라이저를 모두 사용 할 수 있는 상황이라면 부모클래스의 편의 이니셜라이저가 모두 자동으로 상속된다.
class Person {
    var name: String = ""
    var age: Int = 0
    
    init(age: Int) {
        self.age = age
    }
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
    
    convenience init(name: String) {
        self.init(name: name, age: 0)
    }
}

class Student: Person {
    var major: String = "F"
}

class Developer: Person {
    var company: String
    //부모클래스의 지정이니셜라이저를 자식클래스의 편의이니셜라이저로 재정의
    override convenience init(age: Int) {
        self.init(name: "developer", age: age)
        self.company = "Unknown"
    }
    //부모클래스의 지정이니셜라이저 재정의
    override init(name: String, age: Int) {
        self.company = "Unknown"
        super.init(name: name, age: age)
    }

    //자식클래스의 지정 이니셜라이저
    init(name: String, age: Int, company: String) {
        self.company = company
        super.init(name: name, age: 0)
    }
    //자식클래스의 편의 이니셜라이저
    convenience init(company: String) {
        self.init(name: "Unknown", age: 0)
        self.company = company
    }
}

//부모클래스의 지정 이니셜라이저 자동 상속
let yagom: Person = Person(name: "야곰", age: 5)
let haha: Student = Student(name: "하하", age: 20)
print(yagom.name)
print(haha.name)
print()

//부모클래스의 편의 이니셜라이저 자동 상속
let wizplan: Person = Person(name: "wizplan")
let jinsung: Student = Student(name: "jinsung")
print(wizplan.name)
print(jinsung.name)
print()

//부모클래스의 편의 이니셜라이저 자동 상속
let developer: Developer = Developer(name: "개발자")
print(developer.name)
print(developer.company)
print(developer.age)
print()

//부모클래스의 지정 이니셜라이저를 자식클래스에서 편의 이니셜라이저로 재정의
let developer2: Developer = Developer(age: 55)
print(developer2.name)
print(developer2.company)
print(developer2.age)
print()

//자식클래스에서 편의 이니셜라이저를 정의해도 자동 상속에 문제가 없음.
let developer3: Developer = Developer(company: "Naver")
print(developer3.name)
print(developer3.company)
print(developer3.age)

--result--
야곰
하하

wizplan
jinsung

개발자
Unknown
0

developer
Unknown
55

Unknown
Naver
0
  • Person - Student 클래스 관계에서의 자동상속
    • Student의 major 프로퍼티에 기본값이 있으며, 따로 지정 이니셜라이저를 구현해주지 않았으므로 부모클래스인 Person클래스의 지정 이니셜라이저가 자동으로 상속됨.
    • 이는 규칙1에 부합한다.
    • 부모클래스의 지정 이니셜라이저를 모두 자동으로 상속받았으므로 편의 이니셜라이저도 자동으로 상속되었다.
  • Person - Developer 클래스 관계에서의 자동상속
    • Developer 클래스의 company 프로퍼티에 기본값이 없더라도 이니셜라이저에서 적절히 초기화했고, 부모클래스의 지정 이니셜라이저를 모두 재정의하여 부모클래스의 지정 이니셜라이저와 동일한 이니셜라이저를 모두 사용할 수 있는 상황이므로 규칙1 에 부합한다.
      • 따라서 부모클래스의 편의 이니셜라이저가 자동으로 상속되었다.
    • 자동 상속 규칙은 자식클래스에 편의 이니셜라이저를 추가한다고 하더라도 유효하다.
    • 또 부모클래스의 지정 이니셜라이저를 자식클래스의 편의 이니셜라이저로 구현하더라도 규칙2 를 충족한다.

요구 이니셜라이저

  • required 수식어를 클래스의 이니셜라이저 앞에 명시해주면 이 클래스를 상속받은 자식클래스에서 반드시 해당 이니셜라이저를 구현해주어야 한다.
  • 다만 자식클래스에서 요구 이니셜라이저를 재정의할 때는 override 수식어 대신에 required 수식어를 사용한다.
  • 만약 부모클래스에 요구 이니셜라이저가 있지만, 자식클래스의 프로퍼티에 기본값이 있으며 별다른 지정 이니셜라이저가 없다면 자식클래스는 부모클래스의 이니셜라이저를 자동 상속받게 된다.
class Person { 
	var name: String
	required init() { 
		self.name = "Unknown"
	}
}

class Student: Person { 
	var major: String = "Unknown"
}

let miJeong: Student = Student()
  • 그러나 Student 클래스에 새로운 지정 이니셜라이저를 구현한다면 부모클래스로부터 이니셜라이저가 자동으로 상속되지 않으므로 요구 이니셜라이저를 구현해주어야 한다.
class Person { 
	var name: String
	required init() { 
		self.name = "Unknown"
	}
}

class Student: Person { 
	var major: String = "Unknown"
	init(major: String) { 
		self.major = major
		super.init()
	}

	required init() { 
		self.major = "Unknown"
		super.init()
	}
}

let miJeong: Student = Student()
  • 부모클래스의 지정 이니셜라이저를 자식클래스에서 요구 이니셜라이저로 변경 가능하다.
    • required override를 명시해주어 재정의됨과 동시에 요구 이니셜라이저가 될 것임을 명시해준다.
  • 또한 부모클래스의 편의 이니셜라이저를 자식 클래스에서 요구 이니셜라이저로 변경 가능하다.
    • required convenience를 명시해주어 재정의됨과 동시에 요구 이니셜라이저가 될 것임을 명시해준다.
profile
하루하루 꾸준히

0개의 댓글