[스위프트 프로그래밍-18장] 상속

sanghee·2021년 11월 22일
0
post-thumbnail

이 글은 스위프트 프로그래밍(3판, 야곰 지음)을 읽고 간단하게 정리한 글입니다. 책에 친절한 설명과 관련 예제 코드들이 있으므로 직접 사서 읽기를 추천합니다.

18.0 소개

상속이란?

어떤 클래스로부터 속성들을 물려받는 것을 의미한다.

부모클래스(Superclass, Parents-class)와 자식클래스(Subclass, Child-class), 기반 클래스(Base class)

클래스는 다른 클래스로부터 메소드나 프로퍼티 등을 상속받을 수 있다. 자식 클래스는 상속받은 클래스를, 부모클래스는 상속한 클래스를 말한다. 기반 클래스란 상속받지 않은 클래스를 의미한다.

자식클래스는 부모클래스의 메서드, 프로퍼티, 프로퍼티 감시자, 서브스크립트, 이니셜라이저 등을 사용하거나 재정의할 수 있다.

18.1 클래스 상속

자식클래스 이름 뒤에 콜론을 붙이고 상속 받을 부모클래스 이름을 쓴다.

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

Person

class Person {
    var name: String = "사람"
    var age: Int = 0
}

Person → Student

Student 클래스는 Person 클래스를 상속받는다.

Person의 name, age 프로퍼티를 사용할 수 있다.

자신의 메서드인 study()를 추가할 수 있다.

class Student: Person {
    var grade: Int = 100
    
    func study() {
        print("I'm studying...")
    }
}

let student = Student()
student.name
student.age
student.grade
student.study()

Student → UniversityStudent

class UniversityStudent: Student {
    var major: String = "전공"
    
    override func study() {
        print("I'm dying...")
    }
}

let universityStudent = UniversityStudent()
universityStudent.name
universityStudent.age
universityStudent.grade
universityStudent.major
universityStudent.study()

332페이지의 도표 표현하기

18.2 재정의

📌키워드: override

아까 살펴봤던 메서드 study를 재정의했었다.

자식클래스가 부모클래스로부터 상속받은 특성을 그대로 사용하지 않고 변경하여 사용하는 것을 재정의라고 한다. 만약 부모클래스에 해당 특성이 없다면 컴파일 오류가 발생한다.

class UniversityStudent: Student {
    var major: String = "전공"
    
    override func study() {
        print("I'm dying...")
    }
    
    override func work() { // ⛔️ Method does not override any method from its superclass
        print("I'm working...")
    }
}

부모클래스의 특성을 사용하고 싶은 경우

📌키워드: super

super 프로퍼티를 사용하면 된다.

class UniversityStudent: Student {
    var major: String = "전공"
    
    override func study() {
        super.study()
        print("I'm dying...")
    }
}

I'm studying...
I'm dying...

18.2.1 메서드 재정의

인스턴스 메서드

지금까지 사용했던 메서드는 모두 인스턴스 메서드이다. 인스턴스를 먼저 생성한 후 점(.)을 통해 해당 메서드에 접근한다

타입 메서드

📌키워드: static, class

인스턴스 생성 없이 타입을 통해 호출된다. static은 오버라이딩을 금지하지만 class는 오버라이딩을 허용한다.

class Student: Person {
    static func play() {
        print("I'm playing...")
    }

    class func introduce() {
        print("I'm student")
    }
}

class UniversityStudent: Student {
    override class func introduce() {
        print("I'm universityStudent")
    }
}
let student = Student()
student.study() // 인스턴스
Student.study(student)  // 인스턴스
Student.play() // 타입
Student.introduce()

let universityStudent = UniversityStudent()
universityStudent.study() // 인스턴스
UniversityStudent.introduce() // 타입

같은 이름의 메서드?

메서드는 반환 타입이나 매개변수가 다르면 다른 메서드로 취급한다.

play 메서드를 보면 메서드 이름이 동일하지만 반환 타입이 다른, 다른 메서드이다. 재정의없이 사용한다.

introduce 메서드를 보면 매개변수가 다르기에 다른 메서드이다.

class Student: Person {
    func play() {
        print("I'm playing...")
    }

    class func introduce() {
        print("I'm student")
    }
}

class UniversityStudent: Student {
    func play() -> String {
        "I'm playing..."
    }

    func introduce(_ name: String) {
        print("I'm \(name)")
    }

    override class func introduce() {
        print("I'm universityStudent")
    }
}

타입캐스팅

universityStudent의 play라는 이름을 가진 메서드가 두개이기에, 타입캐스팅을 하여 사용한다.

타입캐스팅을 하지 않으면 Ambiguous use of 'play()'라는 에러 메세지가 뜬다. 타입캐스팅은 뒤에서...

let student = Student()
student.play() // 인스턴스 메서드
Student.introduce() // 타입 메서드

let universityStudent = UniversityStudent()
universityStudent.play() // ⛔️ Ambiguous use of 'play()'
universityStudent.play() as Void
universityStudent.play() as String

universityStudent.introduce("대학생")
UniversityStudent.introduce()

18.2.2 프로퍼티 재정의

프로퍼티를 재정의한다는 것은 프로퍼티 자체가 아니라, 접근자, 설정자, 감시자 등을 재정의하는 것을 의미한다.

읽기 전용 프로퍼티를 읽고 쓰기가 가능하도록은 할 수 있지만, 읽고 쓰기가 모두 가능했던 걸 읽기 전용으로 재정의할 수는 없다.

상속 관계 모식도 332p에서 보면, 자식클래스는 부모클래스의 특성을 다 사용할 수 있어야 하기에, 부모에서 읽고 쓸수 있었다면 자식에서도 둘 다 가능해야 한다.

Person → Student

Person 클래스에서는 읽기 전용 프로퍼티인 koreanAge가 있다.

class Person {
    var name: String = "사람"
    var age: Int = 0
    var koreanAge: Int {
        self.age + 1
    }
}

name이라는 프로퍼티 자체를 재정의할 수 없다.

자식클래스인 Student 클래스에서 읽기 전용 프로퍼티인 koreanAge를 읽고 쓰기가 가능하도록 재정의할 수 있다. (반대는 불가능)

class Student: Person {
    override var name: String = "학생" // ⛔️ Cannot override with a stored property 'name'
    var grade: Int = 100
    override var koreanAge: Int {
        get {
            super.koreanAge
        }
        set(newAge) {
            self.age = newAge - 1 // newAge 대신 newValue로 접근가능
        }
    }
}

Person에서 koreanAge는 읽기 전용이었지만 student에서는 쓰는 것도 가능하다.

let person = Person()
person.koreanAge

let student = Student()
student.koreanAge = 10
student.age // 9

⛔️ 읽고 쓰기 가능 → 읽기 전용 X

부모클래스의 koreanAge를 읽고 쓰기가 가능한데 자식클래스에서는 읽기 전용으로 한다면 아래와 같은 에러가 발생한다.

⛔️ Cannot override mutable property with read-only property 'koreanAge'

읽고 쓰기 프로퍼티 수정

부모클래스인 Person으로부터 get을 가져오고 싶다면 super 키워드를 사용한다.

class Person {
    var age: Int = 0
    var koreanAge: Int {
        get {
            self.age + 1
        }
        set(newAge) {
            self.age = newAge - 1 // newAge 대신 newValue로 접근가능
        }
    }
}

class Student: Person {
    override var koreanAge: Int {
        get {
            super.koreanAge
        }
        set {
            self.age = newValue - 2
        }
    }
}

18.2.3 프로퍼티 감시자 재정의

프로퍼티 감시자도 재정의할 수 있다. 주의할 점은 재정의하더라도 부모클래스의 프로퍼티 감시자도 동작한다는 점이다.

class Person {
    var age: Int = 0 {
        didSet {
            print("I'm \(self.age)")
        }
    }
}

class Student: Person {
    override var age: Int {
        didSet {
            print("I'm \(self.age)~~")
        }
    }
}

let student = Student()
student.age = 10

I'm 10
I'm 10~~

18.2.4 서브스크립트 재정의

School → University

School의 서브스크립트는 grades 배열에서, University의 서브스크립트는 universityGrades에서 해당 값을 가져오도록 재졍의하였다.

class School {
    var grades: [String] = ["elementary", "middle", "high"]
    
    subscript(index: Int) -> String {
        (index < grades.count) ? grades[index] : ""
    }
}
class University: School {
    var universityGrades: [String] = ["freshman", "sophomore", "junior", "senior"]
    
    override subscript(index: Int) -> String {
        (index < universityGrades.count) ? universityGrades[index] : ""
    }
}
let school = School()
let university = University()

school[0] // elementary
university[0] // override하지 않았다면? elementary
university[0] // freshman

18.2.5 재정의 방지

📌키워드: final

자식클래스에서 재정의하는 것을 방지하려면?

클래스 자체를 상속하거나 재정의하는 것을 막는다면 클래스 앞에 final을 붙인다. 또는 부모클래스 특성을 재정의하는 걸 막는다면 해당 특성 앞에 final을 붙이면 된다.

class Person {
    final func study() {
        print("...")
    }
}

class Student: Person {
    override func study() {  // ⛔️ Instance method overrides a 'final' instance method
        print("I'm studying...")
    }
}
final class School {
}

class University: School { // ⛔️ Inheritance from a final class 'School'
}

+ final의 또 다른 장점?

메소드 디스패치란?

메소드 디스패치란 어떤 연산을 실행해야 하는지 결정하도록 돕는 메커니즘이다. 더 정확히 말하면 어떤 메소드 구현이 사용되어야 하는지 결정하는 것이다.

정적 디스패치와 동적 디스패치

정적 디스패치는 값, 참조타입에서 모두 지원된다. 하지만 동적 디스패치는 참조타입에서만 지원된다. 동적 디스패치를 위해서는 상속이 필요한데 값 타입은 상속을 지원하지 않기 때문이다. 일단 디스패치는 정적/동적 보다 더 세분화해서 나눌 수 있다. Dynamic으로 갈수록 느리다.

  1. Inline
  2. Static(정적)
  3. Virtual
  4. Dynamic(동적)

속도 차이 이유

정적 디스패치는 컴파일러가 명령(instructions)들이 어디에 있는지 안다는 측면에서 동적 디스패치보다 더 빠르다. 그러므로 함수가 호출되면 컴파일러는 수행할 함수의 메모리 주소로 바로 이동한다. 이는 성능을 크게 향상시킨다.

클래스의 정적 디스패치

클래스의 경우 정적 디스패치를 위해서 final(재정의, 상속 방지), static 키워드(타입 메서드에서 메서드 재정의 방지)를 통해 추가적인 상속이나 재정의가 불가능하도록 만든다.

final, static, class 차이 다시 정리

1. final 은 오버라이딩을 방지하기 위해 사용한다.

2. static과 class의 쓰임은 메소드와 프로퍼티를 인스턴스화 하지 않고 하나만 존재하도록 하기 위해 사용된다. static은 class와 달리 상속이 불가능하다.

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

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

지정 이니셜라이저

모든 프로퍼티를 초기화해야 한다.

클래스는 하나 이상의 지정 이니셜라이저를 갖는다.

자식클래스는 다른 저장 프로퍼티가 없을 경우 이니셜라이저를 갖지 않을 수 있다.

편의 이니셜라이저

초기화를 좀 더 손쉽게 도와주는 역할을 한다. 지정 이니셜라이저를 자신 내부에서 호출한다.

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

let korean = Person(name: "철수", koreanAge: 10)

korean.name // 철수
korean.age // 9

18.3.2 클래스의 초기화 위임

세 가지 규칙이 있다.

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

궁극적으로 기반 클래스(Base class)의 지정 이니셜라이저를 반드시 호출하는 것을 확인할 수 있다.

발표자료 여기까지

18.3.3 2단계 초기화

클래스의 2단계 초기화는 프로퍼티를 초기화하기 전에 프로퍼티 값에 접근하는 것을 막아 초기화를 안전하게 할 수 있도록 한다.

  1. 1단계 → 클래스에 정의한 각각의 저장 프로퍼티에 초깃값이 할당된다.
  2. 2단계 → 저장 프로퍼티들을 사용자 정의할 기회를 얻는다.

1단계

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

2단계

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

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

이니셜라이저

기본적으로 스위프트의 이니셜라이저는 부모클래스의 이니셜라이저를 상속받지 않는다. 자식클래스에 최적화되어있지 않기 때문이다. 안전하다고 판단되는 상황에서만 상속한다.

부모클래스의 편의 이니셜라이저를 자식클래스에서 호출할 수 없다. 따라서 재정의하지 않는다.

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

class Student: Person {
    var major: String
    
    override init(name: String, age: Int) {
        self.major = "iOS"
        super.init(name: name, age: age)
    }
    
    // override convenience 안함
    convenience init(name: String, koreanAge: Int) {
        self.init(name: name, age: koreanAge - 1)
    }
}

실패 가능한 이니셜라이저

부모클래스의 실패 가능한 이니셜라이저를 필요에 따라 실패하지 않도록 재정의할 수 있다.

class Person {
    var name: String
    var age: Int

    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
    
    init?(age: Int) {
        guard age <= 0, age > 150 else { return nil }
        self.name = "이름"
        self.age = age
    }
}

class Student: Person {
    var major: String
    
    override init(age: Int) {
        self.major = "iOS"
        super.init(name: "이름", age: age)
    }
}

let student = Student(age: 100)
student.name // 이름
student.age // 100
student.major // iOS

18.3.5 이니셜라이저 자동 상속

자식클래스에서 프로퍼티 기본값을 모두 제공하였을 때 다음의 두 가지 규칙일 때 이니셜라이저가 자동으로 상속된다.

  1. 자식클래스에서 별도의 지정 이니셜라이저를 구현하지 않는 경우 자동으로 상속된다.
  2. 규칙 1에 따라 자동으로 상속받은 경우 또는 부모클래스의 지정 이니셜라이저를 모두 재정의한 경우 부모클래스의 편의 이니셜라이저가 자동으로 상속된다.

Person → Student

부모클래스의 지정 이니셜라이저를 모두 재정의한 경우로 규칙 2에 해당한다. 따라서 부모의 편의 이니셜라이저가 자동으로 상속되었다.

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

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

let student = Student(name: "학생", koreanAge: 10) // 부모클래스의 편의 이니셜라이저가 자동 상속됨
student.age // 9

18.3.6 요구 이니셜라이저

📌키워드: required

required 수식어를 클래스의 이니셜라이저 앞에 명시해주면, 이 클래스를 상속받은 자식클래스에서 반드시 해당 이니셜라이저를 구현해야 한다. 다만 자식클래스에서 요구 이니셜라이저를 재정의할 때는 override 대신 required를 사용한다.

xib파일에서 많이 봤었음!

Interface Builder란 간단하게 말해서는 xib, nib, storyboard를 사용하여 VIew를 작업하는 것이다. 여기에서 생성되는 초기화 구문이다.

스토리보드나 Xib 파일을 이용해 UI를 구성하여 이를 그대로 사용하기 위해 NSCoding가 필요하다. 인터페이스 파일은 UI의 구성을 xml 형태로 저장하고 있는데, 이 저장한 형태를 사용자의 화면으로 그대로 가져오기 위해 init(coder: NSCoder)를 통해서 객체가 생성된다.

required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}

NSCoding이란 보관 및 배포를 위해 개체를 인코딩 및 디코딩할 수 있도록 하는 프로토콜이다.

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

Person → Student

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

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

let student = Student()

required init에 매개변수가 다른 경우

Person을 상속받는 Student에서 요구 이니셜라이저를 재정의할 때 매개변수를 추가하면 아래와 같은 에러가 발생한다. fatalError 부분을 추가하면 된다.

⛔️ 'required' initializer 'init()' must be provided by subclass of 'Person'
class Person {
    var name: String
    var age: Int
    
    required init() {
        self.name = "이름"
        self.age = 0
    }
}

class Student: Person {
    var major: String
    
    required init(major: String) {
        self.major = major
        super.init()
    }
    
    required init() {
        fatalError("init() has not been implemented")
    }
}

let student = Student(major: "iOS")
student.major // iOS
student.name // 이름
student.age // 0

또는 다음과 같이 작성한다.

class Student: Person {
    var major: String
    
    init(major: String) {
        self.major = major
        super.init()
    }
    
    required init() {
        self.major = "iOS"
        super.init()
    }
}
profile
👩‍💻

0개의 댓글