Swift 정리 - 18. 상속


Swift에서의 상속은 클래스를 다른 타입과 구분짓는 근복적인 행위다. 상속에 대해 알아보자.


Swift.org:Inheritance

클래스는 다른 클래스에서 메서드, 프로퍼티 등을 상속받을 수 있다. 상속을 받는 클래스를 subclass, 상속을 당하는 클래스를 superclass라고 한다.

Swift의 클래스는 부모 클래스에 속하는 메서드, 프로퍼티, 서브스크립트에 접근하고 호출하면서 이런 메서드들을 overriding 해서 자신만의 버전을 가질 수도 있다. Swift는 override된 정의와 일치하는 부분이 부모 클래스에 있는지 확인해 override가 올바른지 확인한다.

클래스는 상속받은 프로퍼티에 프로퍼티 감시자를 추가할 수 있다. 연산 프로퍼티를 정의한 클래스에는 해당 연산 프로퍼티에 감시자를 구현할 수 없지만(할 수도 없고 그럴 필요도 없음. 연산 프로퍼티의 getter, setter를 통해 프로퍼티 감시자를 구현할 수 있기 때문) 부모 클래스에서 정의한 프로퍼티가 있다면 자식 클래스에서 프로퍼티 감시자를 구혀낳ㄹ 수 있다.

Base Class 정의하기

다른 클래스에서 상속받지 않은 클래스를 base class라고 한다.

Swift 클래스는 전역적인 base class를 상속하지 않는다. 부모 클래스를 명시하지 않고 정의한 클래스는 자동으로 base class가 된다.

Vehicle base class

class Vehicle {
    var currentSpeed = 0.0
    var description: String {
        return "traveling at \(currentSpeed) miles per hour"
    }
    func makeNoise() {
        // do nothing - an arbitrary vehicle doesn't necessarily make a noise
    }
}

위 클래스는 일반적인 이동수단에 대한 공통적인 특징을 정의했기 때문에 내부에서 특별히 뭘 할 건 없다. 이를 유용하게 만들기 위해 특정 종류의 이동수단을 정의해서 무언가를 더 정의해야 한다.

Subclassing

Subclassing은 존재하는 클래스를 기반으로 한 새로운 클래스의 행위다. 말이 좀 어색한데, 영어와 한국어의 어감의 차이에서 오는 어색함이라 생각한다. 자식 클래스는 존재하는 클래스의 특징을 상속할 수 있고 이후 이를 개선할 수 있다. 또한 자식 클래스에 새로운 특징을 추가할 수도 있다.

자식 클래스가 부모 클래스를 가지고 있다는 것을 명시하기 위해 자식 클래스 이름 뒤에 콜론을 붙이고 부모 클래스 이름을 쓴다.

class SomeSubclass: SomeSuperclass {
    // subclass definition goes here
}

위에서 생성한 Vehicle base class를 부모 클래스로 한 자식 클래스 Bicycle을 정의하면 아래와 같다.

class Bicycle: Vehicle {
    var hasBasket = false
}

이 Bicycle 자식 클래스는 자동으로 Vehicle 클래스의 모든 특징(프로퍼티, 메서드)을 다 얻는다. 추가로 새로운 저장 프로퍼티인 hasBasket을 정의했다.

let bicycle = Bicycle()
bicycle.hasBasket = true

// 부모 클래스 프로퍼티에 접근
bicycle.currentSpeed = 15.0
print("Bicycle: \(bicycle.description)")
// Bicycle: traveling at 15.0 miles per hour

서브 클래스를 다른 클래스가 또 상속할 수 있다.

class Tandem: Bicycle {
    var currentNumberOfPassengers = 0
}

// 부모 클래스, 부모의 부모 클래스의 특징에 접근
let tandem = Tandem()
tandem.hasBasket = true
tandem.currentNumberOfPassengers = 2
tandem.currentSpeed = 22.0
print("Tandem: \(tandem.description)")
// Tandem: traveling at 22.0 miles per hour

이렇게 다른 클래스를 상속받으면 부모 클래스의 특징을 그대로 사용할 수 있기 때문에 코드의 재사용하기 용이하고, 기능을 확장할 때 기존 클래스를 변경하지 않고도 새로운 추가 기능을 구현한 클래스를 정의할 수 있다.

Overriding

자식 클래스는 부모 클래스에서 상속받은 특징을 overriding해서 자신만의 기능으로 새로 구현할 수 있다. Overriding할 수 있는 특징에는 다음의 것들이 있다.

  1. 인스턴스 메서드
  2. 타입 메서드
  3. 인스턴스 프로퍼티
  4. 타입 프로퍼티
  5. 서브스크립트

오버라이드하려면 override 키워드를 붙인다. 이를 통해 override한다는 것을 명확하게 할 수 있고, 실수로 부모 클래스에 정의되지 않은 메서드를 오버라이딩하는 것을 막는다. 만약 이런 상황이 생기면 컴파일될 때 에러로 표시된다. override 키워드를 쓰면 Swift 컴파일러가 부모 클래스들을 확인해서 오버라이딩한 정의와 일치하는 정의를 갖고 있는지 확인한다.

부모 클래스의 메서드, 프로퍼티, 서브스크립트

만약 자식 클래스에서 부모 클래스의 특성을 오버라이딩했는데, 부모 클래스의 특성을 자식 클래스에서 사용하고 싶으면 super 키워드를 사용한다.

  • 부모 메서드 호출 : super.someMethod()
  • 부모 프로퍼티 호출 : super.someProperty
  • 부모 서브스크립트 호출 : super[someIndex]

메서드 Override

class Train: Vehicle {
    override func makeNoise() {
        print("Choo Choo")
    }
}

let train = Train()
train.makeNoise()
// Prints "Choo Choo"

프로퍼티 Override

상속받은 인스턴스, 타입 프로퍼티를 오버라이드해서

  1. 프로퍼티의 getter, setter를 커스터마이징
  2. 프로퍼티 감시자를 추가

할 수 있다.

1. 프로퍼티 getter, setter override

상속받은 프로퍼티가 저장 프로퍼티인지 연산 프로퍼티인지와는 상관 없이 오버라이드할 수 있다. 상속받은 프로퍼티의 본질은 자식 클래스에서는 보이지 않고, 이름과 타입만 알고 있다. 프로퍼티를 오버라이딩 할 때는 이름, 타입을 꼭 명시해서 컴파일러가 부모 클래스에 같은 프로퍼티가 있는지 확인할 수 있게 해야 한다.

부모 클래스의 읽기 전용 프로퍼티를 자식 클래스에서 getter, setter를 모두 구현해서 읽고 쓰기가 가능하게 할 수 있다. 하지만 읽고 쓰기가 가능한 프로퍼티를 읽기 전용 프로퍼티로는 만들 수 없다.

오버라이드한 프로퍼티의 getter를 부모 클래스와 같게 하고 싶다면 단순히 getter에 super.someProperty와 같이 상속받은 값을 전달한다.

class Car: Vehicle {
    var gear = 1
    override var description: String {
        return super.description + " in gear \(gear)"
    }
}

let car = Car()
car.currentSpeed = 25.0
car.gear = 3
print("Car: \(car.description)")
// Car: traveling at 25.0 miles per hour in gear 3

2. 프로퍼티 감시자 override

상속받은 프로퍼티에 프로퍼티 감시자를 추가할 수 있다.

상속받은 상수 저장 프로퍼티나 상속받은 읽기 전용 연산 프로퍼티에 프로퍼티 감시자를 추가할 수 없다. 이들 프로퍼티는 값이 바뀔 수 없기 때문이다. 또한 오버라이딩 한 프로퍼티에 setter를 override함과 동시에 프로퍼티 감시자를 추가할 수 없다. 만약 값이 변하는 걸 추적하고 싶으면 오버라이딩한 setter에서 구현하면 되기 때문이다.

프로퍼티 감시자를 재정의해도 부모 클래스에 정의한 프로퍼티 감시자도 동작한다. 부모 클래스에 정의된 프로퍼티 감시자가 먼저 호출되고 나서 자식 클래스에서 오버라이딩한 프로퍼티 감시자가 호출된다.

Sdfsdfs

class AutomaticCar: Car {
    override var currentSpeed: Double {
        didSet {
            gear = Int(currentSpeed / 10.0) + 1
        }
    }
}

let automatic = AutomaticCar()
automatic.currentSpeed = 35.0
print("AutomaticCar: \(automatic.description)")
// AutomaticCar: traveling at 35.0 miles per hour in gear 4

Override 방지

final 키워드를 붙여서 메서드, 프로퍼티, 서브스크립트가 오버라이딩 되는 것을 막을 수 있다. Introducer 키워드 앞에 final modifier를 붙이면 된다.(final var, final func, final class func)

final 키워드가 붙여진 특성을 오버라이드하려고 하면 컴파일 타임 에러가 발생한다. 전체 클래스를 final하게 만들면 다른 클래스에서 상속할 수 없게 된다.

Class 상속과 Initialization

Swift.org:Class Inheritance and Initialization

클래스의 모든 저장 프로퍼티(부모 클래스에서 상속받은 것 포함)는 초기화 도중 초깃값을 가져야 한다.

Swift는 클래스 타입에 두 종류의 이니셜라이저를 제공한다.

  1. Designated initializer, 지정 이니셜라이저
  2. Convenience initializer, 편의 이니셜라이저

Designated Initializer, Convenience Initializer

  • 지정 이니셜라이저 : 클래스의 메인 이니셜라이저. 클래스의 모든 프로퍼티를 완전히 초기화하고 필요에 따라 부모 클래스의 이니셜라이저를 호출할 수 있다. 클래스는 일반적으로 적은 수나 하나의 지정 이니셜라이저를 갖는다. 지정 이니셜라이저는 초기화가 일어나는 과정에서 지나쳐야 하는 부분과 같아서 부모 클래스의 초기화가 이어서 진행된다. 모든 클래스는 최소 하나의 지정 이니셜라이저를 갖고 있어야 한다.
  • 편의 이니셜라이저 : 클래스의 이니셜라이저를 지원해주는 보조 이니셜라이저. 내부에서 지정 이니셜라이저를 호출한다. 클래스는 편의 이니셜라이저를 구현하지 않아도 된다. 편의 이니셜라이저를 통해 특정 값을 가지는 인스턴스를 편리하게 생성할 수 있다.

Designated and Convenience Initializer 문법

지정 이니셜라이저

image

편의 이니셜라이저

init키워드 전에 convenience 키워드를 붙인다.

image

클래스 타입의 Initializer Delegation

지정 이니셜라이저와 편의 이니셜라이저간의 관계를 간단히 하기 위해 Swift는 이니셜라이저간의 delegation 호출에 세 가지 원칙을 적용한다.

  1. 지정 이니셜라이저는 직속 부모 클래스의 지정 이니셜라이저를 호출한다.
  2. 편의 이니셜라이저는 같은 클래스 내의 다른 이니셜라이저를 호출한다.
  3. 편의 이니셜라이저는 궁극적으로 지정 이니셜라이저를 호출한다.

이를 기억하는 간단한 방법은 다음과 같다.

  • 지정 이니셜라이저는 항상 로 초기화를 위임한다.
  • 편의 이니셜라이저는 항상 으로 초기화를 위임한다.

image

좀 더 복잡해진 계층에서는 아래와 같이 되는데, 지정 이니셜라이저가 꼭 통과해야 하는 지점과 같이 동작하는 것을 잘 보여준다.

image

2단계 초기화

Swift에서 클래스 초기화는 두 단계로 이루어진다.

  1. 1단계 : 각 저장 프로퍼티는 해당 프로퍼티를 정의한 클래스에 의해 초깃값이 할당된다. 모든 저장 프로퍼티의 초기 상태가 정의되면 다음 단계로 넘어간다.
  2. 2단계 : 각 클래스는 새로운 인스턴스가 준비되기 전에 저장 프로퍼티를 커스터마이징 할 수 있다.

이 2단계 초기화를 통해 초기화를 안전하게 만들 수 있으면서 클래스 계층의 각 클래스에 유연성을 줄 수 있다. 초기화 되기 전에 프로퍼티 값에 접근하는 것을 예방하고, 실수로 다른 이니셜라이저에 의해 다른 값으로 설정되는 걸 막는다.

Objective-C에서는 초기화 1단계에서 모든 프로퍼티에 0또는 null을 할당하지만 Swift는 이 값 대신 내가 설정한 초깃값을 설정할 수 있게 한다.

Swift 컴파일러는 safety-check를 통해 2단계 초기화가 에러 없이 수행되게 한다.

Safety Check 1

지정 이니셜라이저는 클래스에서 정의된 모든 프로퍼티가 부모 클래스의 이니셜라이저를 호출하기 전에 초기화 해야 한다.

객체의 메모리는 모든 저장 프로퍼티의 초기 상태가 정해졌을 때 완전히 초기화되었다는 것으로 간주된다. 이를 만족하기 위해 체인의 위(부모 클래스)로 가기 전에 자신에서 정의된 프로퍼티가 초기화됐음을 확인하는 것이다.

Safety Check 2

지정 이니셜라이저는 상속 받은 프로퍼티에 값을 할당하기 전에 부모 클래스의 이니셜라이저를 호출해야 한다. 그렇지 않다면 지정 이니셜라이저가 할당한 값은 부모 클래스의 초기화 과정 중 덮어씌워질 것이다.

Safety Check 3

편의 이니셜라이저는 어떤 프로퍼티라도 값을 할당하기 전에 다른 이니셜라이저를 호출해야 한다. 그렇지 않으면 편의 이니셜라이저가 할당한 값은 같은 클래스 내의 지정 이니셜라이저에 의해 덮어씌워진다.

Safety Check 4

이니셜라이저는 초기화 1단계가 끝날때까지 인스턴스 메서드를 호출하거나, 인스턴스 프로퍼티의 값을 읽거나, self를 쓸 수 없다.

클래스 인스턴스는 초기화 1단계가 끝나기 전까지 유효하지 않다.

위에서 본 초기화 2단계에서 4개의 safety check가 어떻게 맞물려 작동하는지 보자.

1단계

  • 클래스의 지정 이니셜라이저 혹은 편의 이니셜라이저가 호출된다.
  • 해당 클래스의 새로운 인스턴스를 위한 메모리가 할당되었지만 초기화되지는 않았다.
  • 클래스의 지정 이니셜라이저가 해당 클래스에 정의된 모든 저장 프로퍼티가 값이 있는지 확인한다. 이 저장 프로퍼티의 메모리가 이제 초기화됐다.
  • 지정 이니셜라이저는 부모 클래스의 이니셜라이저에게 초기화를 양도해서 저장 프로퍼티에 같은 작업을 수행한다.
  • 마지막으로 호출된 클래스는 모든 저장 프로퍼티가 값을 갖고 있다는 것을 보장하고, 인스턴스 메모리는 완전히 초기화 된 것으로 간주된다.

image

2단계

  • 최상위 클래스(가장 마지막에 호출된 클래스)에서 처음에 호출했던 클래스로 돌아오며 각 클래스의 지정 이니셜라이저는 인스턴스를 커스터마이징할 수 있다. 이니셜라이저는 self를 호출하고 프로퍼티를 수정하고, 인스턴스 메서드를 호출할 수 있다.
  • 편의 이니셜라이저는 인스턴스를 커스터마이징할 수 있고 self를 사용할 수 있다.

image

Initializer Inheritance and Overriding

Swift의 자식 클래스들은 부모 클래스의 이니셜라이저를 상속받지 않는다. 부모 클래스에서 상속받은 이니셜라이저가 자식의 모든 프로퍼티를 초기화한다는 보장이 없기 때문에 자식 클래스의 인스턴스가 완전히 초기화 되지 않는 상황을 막기 위함이다.

부모 클래스의 이니셜라이저는 안전하고, 적절한 상황일 때 상속될 수 있다.

자식 클래스에서 부모 클래스의 이니셜라이저를 사용하고 싶으면 자식 클래스에서 똑같은 이니셜라이저를 구현해주면 된다. 자식 클래스에서 부모 클래스의 지정 이니셜라이저와 같은 이니셜라이저를 구현하면 부모 클래스의 지정 이니셜라이저를 override하게 되는 것이다. 따라서 자식 클래스 이니셜라이저를 정의할 때 override modifier를 붙여야 한다. 제공된 기본 이니셜라이저를 override할 때도 마찬가지다. 부모 클래스의 지정 이니셜라이저를 자식 클래스에서 편의 이니셜라이저로 구현해도 override 키워드를 붙여야 한다.

반대로 부모 클래스의 편의 이니셜라이저와 일치하는 이니셜라이저를 자식 클래스에서 정의하면 부모 클래스의 편의 이니셜라이저는 자식 클래스에서 절대로 직접적으로 호출될 수 없다. 따라서 엄밀히 말해 자식 클래스에서 override를 하지 않는 것이라고 보기 때문에 자식 클래스에서 override modifier를 붙이지 않아도 된다.

또한 부모 클래스의 실패 가능한 이니셜라이저를 자식 클래스에서 실패 가능한 이니셜라이저나 실패하지 않는 이니셜라이저로 정의할 수 있다.

// 저장 프로퍼티에만 기본 값이 있고, 커스텀 이니셜라이저가 없기 때문에 자동으로 default 이니셜라이저를 사용할 수 있다
class Vehicle {
    var numberOfWheels = 0
    var description: String {
        return "\(numberOfWheels) wheel(s)"
    }
}

let vehicle = Vehicle()
print("Vehicle: \(vehicle.description)")
// Vehicle: 0 wheel(s)

class Bicycle: Vehicle {
    // custom 지정 이니셜라이저, 부모 클래스의 지정 이니셜라이저와 일치하기 때문에 override 키워드를 붙임
    override init() {
        super.init()
        numberOfWheels = 2
    }
}

let bicycle = Bicycle()
print("Bicycle: \(bicycle.description)")
// Bicycle: 2 wheel(s)

class Hoverboard: Vehicle {
    var color: String
    init(color: String) {
        self.color = color
        // super.init() implicitly called here
    }
    override var description: String {
        return "\(super.description) in a beautiful \(color)"
    }
}

자동 이니셜라이저 상속

자식 클래스는 부모 클래스의 이니셜라이저를 상속하지 않는다. 하지만 특정 조건에서는 부모 클래스의 이니셜라이저가 자동으로 상속된다. 이를 통해 이니셜라이저 override를 여러 번 할 필요가 없게 된다.

자식 클래스에서 모든 프로퍼티의 기본 값을 제공한다고 가정할 때 두 가지 규칙이 적용된다.

  1. 자식 클래스가 지정 이니셜라이저를 갖지 않는다면 자동으로 부모 클래스의 모든 지정 이니셜라이저를 상속받는다.
  2. 자식 클래스가 부모 클래스의 모든 지정 이니셜라이저를 구현하고 있다면(1번 규칙을 통해 상속받거나 정의에서 직접 구현하거나) 자동으로 부모 클래스의 모든 편의 이니셜라이저를 상속받는다.

자식 클래스는 부모 클래스의 지정 이니셜라이저를 편의 이니셜라이저로 구현할 수도 있다.

Overriding Failable Initializer

부모 클래스의 실패 가능한 이니셜라이저를 오버라이딩할 경우에는 자식 클래스에서는 실패하지 않게 할 수 있다.

class Document {
    var name: String?
    // this initializer creates a document with a nil name value
    init() {}
    // this initializer creates a document with a nonempty name value
    init?(name: String) {
        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
        }
    }
}

class UntitledDocument: Document {
    override init() {
        super.init(name: "[Untitled]")!
    }
}