개발하는 뚝딱이

[swift] 상속 본문

swift

[swift] 상속

개발자뚝딱이 2020. 9. 5. 17:30

 

책 <스위프트 프로그래밍 3판 ;야곰 지음> 을 정리한 글입니다.

 

- 클래스 상속

- 재정의

- 클래스의 이니셜라이저

 


 

1. 상속 (Inheritance)

 

클래스는 메서드나 프로퍼티 등을 다른 클래스로부터 상속받을 수 있으며, 부모클래스-자식클래스의 관계가 성립됨

자식클래스는 부모클래스의 메서드, 프로퍼티 등을 물려받고 재정의하거나, 자신의 기능을 추가할 수 있음

class 클래스이름: 부모클래스이름 {
    // 프로퍼티와 메서드들
}

 

 

// Person - Student - UniversityStudent
// 상속 관계

class Person {
    var name: String = ""
    var age: Int = 0
    
    var introduction: String {
        return "이름:\(name), 나이:\(age)"
    }
    
    func speak() {
        print("가나다라마바사")
    }
}


class Student: Person {
    var grade: String = "F"
    
    func study() {
        print("Study hard...")
    }
}

class UniversityStudent: Student {
    var major: String = ""
}




let yagom: Person = Person()
yagom.name = "yagom"
yagom.age = 99
print(yagom.introduction) // 이름:yagom, 나이:99
yagom.speak() // 가나다라마바사

let jay: Student = Student()
jay.name = "jay"
jay.grade = "A"
print(jay.introduction) // 이름:jay, 나이:0
jay.speak() // 가나다라마바사
jay.study() // Study hard...

let jenny: UniversityStudent = UniversityStudent()
jenny.major = "Art"
jenny.speak() // 가나다라마바사
jenny.study() // Study hard...

 

- 다른 클래스를 상속받으면 똑같은 기능을 구현하기 위해 코드를 다시 작성할 필요 없음 (코드 재사용 용이)

- 기능을 확장할 때 기존 클래스를 변경하지 않고 새로운 추가 기능을 구현한 클래스를 정의할 수 있음

 

 

 

2. 재정의 (Override)

- 재정의 : 부모클래스로부터 물려받은 특성을 그대로 사용하지 않고, 자신만의 기능으로 변경하여 사용

- override 키워드를 통해 스위프트컴파일러가 조상클래스에 해당 특성이 있는지 확인

- 만약 재정의했을 때, 부모클래스의 특성을 자식클래스에서 사용하고 싶다면 super 프로퍼티를 사용

- 재정의한 서브스크립트에서 부모 버전의 서브스크립트로 접근하고 싶으면 super[index] 로 접근

 

 

2.1 메서드 재정의

// Person - Student - UniversityStudent
// 상속 관계

class Person {
    var name: String = ""
    var age: Int = 0
    
    var introduction: String {
        return "이름:\(name), 나이:\(age)"
    }
    
    func speak() {
        print("가나다라마바사")
    }
    
    class func introduceClass() -> String {
        return "인류의 소원은 평화입니다"
    }
}

class Student: Person {
    var grade: String = "F"
    
    func study() {
        print("Study hard...")
    }
    
    override func speak() { // 메서드 재정의
        print("저는 학생입니다")
    }
}

class UniversityStudent: Student {
    var major: String = ""
    
    class func introduceClass() {
        print(super.introduceClass())
    }
    
    override class func introduceClass() -> String { // 메서드 재정의
        return "대학생의 소원은 A+입니다"
    }
    
    override func speak() { // 메서드 재정의
        super.speak() // 부모 메서드 접근
        print("대학생이죠.")
    }
}


let yagom: Person = Person()
yagom.speak() // 가나다라마바사

let jay: Student = Student()
jay.speak() // 저는 학생입니다

let jenny: UniversityStudent = UniversityStudent()
jenny.speak() // 저는 학생입니다 대학생이죠.

print(Person.introduceClass()) // 인류의 소원은 평화입니다
print(Student.introduceClass()) // 인류의 소원은 평화입니다
print(UniversityStudent.introduceClass() as String) // 대학생의 소원은 A+입니다
UniversityStudent.introduceClass() as Void // 인류의 소원은 평화입니다

 

2.2 프로퍼티 재정의

// Person - Student - UniversityStudent
// 상속 관계

class Person {
    var name: String = ""
    var age: Int = 0
    var koreanAge: Int {
        return self.age + 1
    }
    
    var introduction: String {
        return "이름:\(name), 나이:\(age)"
    }
}


class Student: Person {
    var grade: String = "F"
    
    override var introduction: String {
        return super.introduction + ", " + "학점:\(self.grade)"
    }
    
    override var koreanAge: Int { // setter만 가질 수 없음
        get {
            return super.koreanAge
        }
        
        set {
            self.age = newValue - 1
        }
    }
}

let yagom: Person = Person()
yagom.name = "yagom"
yagom.age = 55
//yagom.koreanAge = 56 // error
print(yagom.introduction) // 이름:yagom, 나이:55
print(yagom.koreanAge) // 56

let jay: Student = Student()
jay.name = "jay"
jay.age = 14
jay.koreanAge = 15
print(jay.introduction) // 이름:jay, 나이:14, 학점:F
print(jay.koreanAge) // 15

 

2.3 프로퍼티 감시자 재정의

- 상수 저장 프로퍼티나 읽기 전용 프로퍼티는 감시자를 재정의할 수 없음 (값을 재정의할 수 없으므로)

- 프로퍼티 감시자를 재정의하더라도 조상클래스에서 재정의한 감시자도 동작됨

- 프로퍼티의 접근자와 감시자는 동시에 재정의 할 수 없음

 

2.4 서브스크립트 재정의

- 오버로딩 가능이 가능하므로, 부모클래스의 서브스크립트와 argument와 return type이 같아야 함

class School {
    var students: [Student] = [Student]()
    
    subscript(number: Int) -> Student {
        print("School subscript")
        return students[number]
    }
}

class MiddleSchool: School {
    var middleStudents: [Student] = [Student]()
    
    override subscript(number: Int) -> Student {
        print("MiddleSchool Subscript")
        return middleStudents[number]
    }
}

let university: School = School()
university.students.append(Student())
university[0] // School subscript

let middle: MiddleSchool = MiddleSchool()
middle.middleStudents.append(Student())
middle[0] // MiddleSchool Subscript

 

2.5 재정의 방지

- 특정 특성을 재정의 할 수 없도록 제한하고 싶다면 그 특성 앞에 final 키워드를 명시하면 됨

- 클래스를 상속하거나 재정의 할 수 없다면 class 키워드 앞에 final을 명시해주면 됨

class Person {
    final var name: String = ""
    
    final func speak() {
        print("가나다라마바사")
    }
}

final class Student: Person {
    override var name: String { // error ; Person에서 name의 재정의를 방지함
        set {
            super.name = newValue
        }
        
        get {
            return "학생"
        }
    }
}

class UniversityStudent: Student {  } // error ; Student 클래스는 final로 상속 불가능함

 

 

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

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

- 지정 이니셜라이저 (Designated Initializer) ; 클래스의 주요 이니셜라이저

- 필요에 따라 부모클래스의 이니셜라이저를 호출할 수 있으며, 모든 프로퍼티를 초기화해야 함

- 클래스의 이니셜라이저 중 기둥과 같은 역할이므로 하나 이상 정의 필요

init(매개변수들...) {
  초기화 구문
}

 

- 편의 이니셜라이저 (Convenience Initializer); 초기화를 좀 더 손쉽게 도와주는 역할

- 지정 이니셜라이저를 자신 내부에서 호출

- 지정 이니셜라이저의 매개변수가 많아 외부에서 일일이 전달인자를 전달하기 어렵거나

   특정 목적에서 사용하기 위해 편의 이니셜라이저를 설계할 수도 있음

convenience init(매개변수들...) {
  초기화 구문
}

 

3.2 클래스의 초기화 위임

- 지정 이니셜라이저와 편의 이니셜라이저 사이의 관계 (사실 그림으로 이해하는게 더 빠름...)

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

 

 

3.3 2단계 초기화

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

- 1단계 ; 클래스에 정의한 각각의 저장프로퍼티에 초깃값이 할당

- 2단계 ;  저장 프로퍼티들은 사용자 정의할 기회를 얻음 (초기화를 안전하게 함)

 

- 스위프트 컴파일러는 2단계(two-phase) 초기화를 오류 없이 처리하기 위해 아래 네 가지 안전확인(self-checks)을 실행함

1. 자식클래스의 지정 이니셜라이저가 자신의 프로퍼티를 모두 초기화했는지 확인
2. 자식클래스의 지정 이니셜라이저는 부모클래스의 이니셜라이저를 호출
3. 편의 이니셜라이저는 그 어떤 프로퍼티라도 값을 할당하기 전 다른 이니셜라이저를 호출해야 함
4. 초기화 1단계를 마치기 전까지 인스턴스 메서드 호출X, 인스턴스 프로퍼티의 값을 읽을 수 X,
   self 프로퍼티를 자신의 인스턴스를 나타내는 값으로 활용X

 

- 1단계

1. 클래스가 지정 또는 편의 이니셜라이저 호출
2. 그 클래스의 새로운 인스턴스를 위한 메모리가 할당
3. 지정 이니셜라이저는 클래스에 정의된 모든 저장 프로퍼티에 값이 있는지 확인
   현재 클래스 부분까지의 저장 프로퍼티를 위한 메모리는 이제 초기화됨
4. 지정 이니셜라이저는 부모클래스의 이니셜라이저가 같은 동작을 행할 수 있도록 초기화를 양도
5. 부모클래스는 상속 체인을 따라 최상위 클래스에 도달할 때까지 이 작업을 반복
6. 최상위 클래스에 도달했을 때, 최상위 클래스의 모든 저장 프로퍼티에 값이 있다고 확인되면 인스턴스의 메모리는 모두 초기화됨

 

- 2단계

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

 

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

class Student: Person {
    var major: String
    
    init(name: String, age: Int, major: String) {
        self.major = "Swift" // (1) 확인
        super.init(name: name, age: age) // (2) 확인
    }
    
    convenience override init(name: String, age: Int) {
        self.init(name: name, age: 7, major: "") // (3) 확인
    }
}

// (4) 확인
///// 안전확인 후, super.init을 통해 1단계와 2단계의 초기화를 마침

 

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

- 기본적으로 스위프트의 이니셜라이저는 부모클래스의 이니셜라이저를 상속받지 않음

  (물려받은 이니셜라이저는 자식클래스에 최적화되어 있지 않기 때문)

- 안전하고 적절하다고 판단되는 특정 상황에서는 부모클래스의 이니셜라이저를 상속받기도 함

- 부모 클래스와 동일한 지정 이니셜라이저는 override를 통해 재정의 가능하지만, 부모클래스의 편의 이니셜라이저와

   동일한 이니셜라이저를 자식클래스에 구현할 때는 override를 붙이지 않음.

- 자식클래스에서 부모클래스의 편의 이니셜라이저는 절대로 호출 X

// 클래스 이니셜라이저의 재정의

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: 0)
    }
}

class Student: Person {
    var major: String
    
    override init(name: String, age: Int) {
        self.major = "Swift"
        super.init(name: name, age: age)
    }
    
    convenience init(name: String) { // override 하지 않음
        self.init(name: name, age: 7)
    }
}

 

- 부모클래스가 실패 가능한 이니셜라이저였더라도 자식클래스에서 필요에 따라 실패하지 않는 이니셜라이저로 재정의 가능

// 실패 가능한 이니셜라이저의 재정의

class Person {
    var name: String
    var age: Int
    
    init() { // 지정 이니셜라이저
        self.name = "Unknown"
        self.age = 0
    }
    
    init?(name: String, age: Int) { // 실패 가능한 이니셜라이저
        if name.isEmpty {
            return nil
        }
        self.name = name
        self.age = age
    }
    
    init?(age: Int) { // 실패 가능한 이니셜라이저
        if age < 0 {
            return nil
        }
        self.name = "Unknown"
        self.age = age
    }
}

class Student: Person {
    var major: String
    
    override init?(name: String, age: Int) { // 실패 가능한 이니셜라이저 (재정의)
        self.major = "Swift"
        super.init(name: name, age: age)
    }
    
    override init(age: Int) { // 실패하지 않는 이니셜라이저 (재정의)
        self.major = "Swift"
        super.init()
    }
}

 

3.5 이니셜라이저 자동 상속

- 자식 클래스에서 프로퍼티 기본값을 모두 제공한다고 가정할 때, 아래 두 가지 규칙으로 이니셜라이저가 자동 상속됨

 

1. 자식클래스에서 별도의 지정 이니셜라이저를 구현하지 않는다면, 부모클래스의 지정 이니셜라이저가 자동으로 상속됨

2. 1번 규칙에 따라 자식클래스에서 부모클래스의 지정 이니셜라이저를 자동으로 상속받는 경우 또는 부모클래스의 지정 이니셜라이저를 모두 재정의하여 부모클래스와 동일한 지정 이니셜라이저를 모두 사용할 수 있는 상황이라면 부모클래스의 편의 이니셜라이저가 모두 자동으로 상속됨

 

- 이니셜라이저 자동 상속 (1번 규칙 부합)

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

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

// 부모클래스의 지정 이니셜라이저 자동 상속
let yagom: Person = Person(name: "yagom")
let hana: Student = Student(name: "hana")
print(yagom.name) // yagom
print(hana.name) // hana

let wizplan: Person = Person()
let jinSung: Student = Student()
print(wizplan.name) // Unknown
print(jinSung.name) // Unknown

 

- 편의 이니셜라이저 자동 상속1

- Student 클래스의 mjaor 프로퍼티에 기본값이 없더라도 이니셜라이저에서 적절히 초기화함

- 부모클래스의 지정 이니셜라이저를 모두 재정의하여 부모클래스의 지정 이니셜라이저와 동일한 이니셜라이저를 모두 사용할 수 있는 상황이므로 규칙1에 부합

- 따라서 부모클래스의 편의 이니셜라이저가 자동으로 상속됨

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

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

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

 

- 편의 이니셜라이저 자동 상속2

- Student 클래스에서 부모클래스의 지정 이니셜라이저인 init(name:)을 편의 이니셜라이저로 재정의했지만 부모의 지정 이니셜라이저를 모두 사용할 수 있는 상황인 규칙2에 부합하므로 부모클래스의 편의 이니셜라이저를 사용할 수 있음

- 자신만의 편의 이니셜라이저인 convenience init(major:)를 구현해줬지만 편의 이니셜라이저 자동 상속에는 아무런 영향을 미치지 않음

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

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

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

 

- 편의 이니셜라이저 자동 상속3

- Student 클래스를 상속받은 UniversityStudent 클래스는 grade에 기본값이 있고, 별도의 지정 이니셜라이저를 구현해주지 않았으므로 규칙1에 부합

- 따라서 부모클래스의 이니셜라이저를 모두 자동 상속받음

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

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

class UniversityStudent: Student {
    var grade: String = "A+"
    var description: String {
        return "\(self.name) \(self.major) \(self.grade)"
    }
    
    convenience init(name: String, major: String, grade: String) {
        self.init(name: name, major: major)
        self.grade = grade
    }
    
}

let nova: UniversityStudent = UniversityStudent()
print(nova.description) // Unknown Unknown A+

let raon: UniversityStudent = UniversityStudent(name: "raon")
print(raon.description) // raon Unknown A+

let joker: UniversityStudent = UniversityStudent(name: "joker", major: "Programming")
print(joker.description) // joker Programming A+

let chope: UniversityStudent = UniversityStudent(name: "chope", major: "Computer", grade: "A0")
print(chope.description) // chope Computer A0

 

3.6 요구 이니셜라이저

- required 수식어를 클래스의 이니셜라이저 앞에 명시

- 클래스를 상속받은 자식클래스에서 반드시 해당 이니셜라이저를 구현해줘야 함

- 자식클래스에서 요구 이니셜라이저를 재정의할 때는 override 대신에 required 키워드 사용

 

- 요구 이니셜라이저 정의

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

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

let miJeong: Student = Student()

- Student 클래스에서 요구 이니셜라이저를 구현하지 않음

- major 프로퍼티에 기본값이 있고, 별다른 지정 이니셜라이저가 없기 때문에 이니셜라이저가 자동 상속된 것

 

- 요구 이니셜라이저 재구현

class Person {
    var name: String
    
    init(name: String) {
        self.name = name
    }
    
    // 요구 이니셜라이저 정의
    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()
    }
}

class UniversityStudent: Student {
    var grade: String
    
    // 자신의 지정 이니셜라이저 구현
    init(grade: String) {
        self.grade = grade
        super.init()
    }
    
    required init() {
        self.grade = "A+"
        super.init()
    }
}

let jiSoo: Student = Student()
print(jiSoo.major) // Unknown

let yagom: Student = Student(major: "Swift")
print(yagom.major) // Swift

let juHyun: UniversityStudent = UniversityStudent(grade: "A+")
print(juHyun.grade) // A+

- Student와 UniversityStudent 클래스에서 자신만의 지정 이니셜라이저 구현 → 부모클래스의 이니셜라이저를 자동 상속받지 못함

- Person 클래스에 정의한 요구 이니셜라이저를 자동 상속 규칙에 부합하지 않는 자식클래스인 Student에도 구현해주고, 그 자식클래스인 UniversityStudent 클래스에도 구현해줘야 함

- 이니셜라이저 자동 상속 규칙에 부합하지 않는 한, 요구 이니셜라이저는 반드시 구현해주어야 함

 

- 부모클래스의 일반 이니셜라이저를 자신의 클래스부터 요구 이니셜라이저로 변경할 수 있음

- required override를 명시해주어 재정의됨과 동시에 요구 이니셜라이저가 될 것임을 명시해줘야 함

- 마찬가지로 편의 이니셜라이저도 요구 이니셜라이저로 변경될 수 있음

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

class Student: Person {
    var major: String = "Unknown"
    
    init(major: String) {
        self.major = major
        super.init()
    }
    
    // 부모클래스의 이니셜라이저를 재정의함과 동시에
    // 요구 이니셜라이저로 변경됨을 알림
    required override init() {
        self.major = "Unknown"
        super.init()
    }
    
    // 이 요구 이니셜라이저는 앞으로도 계속 요구됨
    required convenience override init(name: String) {
        self.init()
        self.name = name
    }
}

class UniversityStudent: Student {
    var grade: String
    
    init(grade: String) {
        self.grade = grade
        super.init()
    }
    
    // Student 클래스에서 요구했으므로 구현해줘야 함
    required init() {
        self.grade = "F"
        super.init()
    }
    
    // Student 클래스에서 요구했으므로 구현해줘야 함
    required convenience init(name: String) {
        self.init()
        self.name = name
    }
}

let yagom: UniversityStudent = UniversityStudent()
print(yagom.grade) // F

let juHyun: UniversityStudent = UniversityStudent(name: "JuHyun")
print(juHyun.name) // JuHyun

'swift' 카테고리의 다른 글

[swift] 프로토콜 초기구현  (0) 2020.09.28
[swift] 프로토콜  (0) 2020.09.21
[swift] 서브스크립트  (0) 2020.06.25
[swift] 모나드  (0) 2020.06.23
[swift] 타입캐스팅  (0) 2020.05.17