[Swift] Ch18. 상속

JJUDEV·2024년 4월 29일
1

Swift

목록 보기
15/21
post-thumbnail

본 글은 야곰의 스위프트 프로그래밍: Swift5 교재를 토대로 공부한 내용과 찾아본 내용을 요약한 것입니다.

클래스는 메서드나 프로퍼티 등을 다른 클래스로부터 상속받을 수 있습니다. 어떤 클래스로부터 상속을 받으면 상속받은 클래스는 자식클래스(Subclass, Child-class)라고 표현하며 그 특성을 물려준 클래스는 부모클래스(Superclass, Parents-class)라고 표현합니다. 다른 클래스로부터 상속을 받지 않은 클래스를 기반 클래스(Basic Class)라고 부릅니다.

18.1 클래스 상속

class Animal {
    var name: String
    
    init(name: String) {
        self.name = name
    }
    
    func makeSound() {
        print("Animal sound")
    }
}

class Dog: Animal {
    override func makeSound() {
        super.makeSound()
        print("Woof!")
    }
}

class Cat: Animal {
    override func makeSound() {
        super.makeSound()
        print("Meow!")
    }
}

위의 예제에서 Animal 클래스는 모든 동물의 공통 특성인 name 속성과 makeSound() 메서드를 가지고 있습니다. 그리고 Dog 클래스와 Cat 클래스는 각각 Animal 클래스를 상속받아 각각의 특성을 추가하고 있습니다.

Dog 클래스는 makeSound() 메서드를 재정의하여 Animal 클래스의 소리를 출력한 후에 "Woof!"라는 추가적인 소리를 출력하고 있습니다. 마찬가지로, Cat 클래스도 makeSound() 메서드를 재정의하여 Animal 클래스의 소리를 출력한 후에 "Meow!"라는 추가적인 소리를 출력하고 있습니다.

let dog = Dog(name: "Buddy")
let cat = Cat(name: "Whiskers")

dog.makeSound() // 출력: "Animal sound", "Woof!"
cat.makeSound() // 출력: "Animal sound", "Meow!"

위와 같이 dog, cat 객체를 생성하고 메서드를 호출할 수 있습니다.

18.2 재정의

상속받은 특성들을 재정의하려면 override라는 키워드를 사용합니다. 만약 조상클래스에 재정의할 해당 특성이 없는데 override 키워드를 사용하면 컴파일 오류가 발생합니다.

override func someMethod() { }
// Method does not override any method from its superclass

1) 메서드 재정의

class Vehicle {
    func makeSound() {
        print("Generic sound")
    }
}

class Car: Vehicle {
    override func makeSound() {
        print("Vroom!")
    }
}

class Bicycle: Vehicle {
    override func makeSound() {
        print("Ring ring!")
    }
}

// 객체 생성
let car = Car()
let bicycle = Bicycle()

// 재정의된 메서드 호출
car.makeSound() // 출력: "Vroom!"
bicycle.makeSound() // 출력: "Ring ring!"

위의 예시에서 Vehicle 클래스는 모든 차량에 공통적인 속성과 메서드를 가지고 있습니다. 그리고 Car 클래스와 Bicycle 클래스는 각각 Vehicle 클래스를 상속받아 각각의 특성을 추가하고 있습니다.

Car 클래스와 Bicycle 클래스에서 makeSound() 메서드를 재정의하여 각각의 차량의 소리를 표현하고 있습니다. 이렇게 하면 Car 객체의 makeSound() 메서드가 호출될 때 "Vroom!"이 출력되고, Bicycle 객체의 makeSound() 메서드가 호출될 때 "Ring ring!"이 출력됩니다.

2) 프로퍼티 재정의

class Person {
    var name: String
    
    init(name: String) {
        self.name = name
    }
    
    var description: String {
        return "Name: \(name)"
    }
}

class Student: Person {
    var studentID: String
    
    init(name: String, studentID: String) {
        self.studentID = studentID
        super.init(name: name)
    }
    
    // 저장 프로퍼티 재정의
    override var name: String {
        get {
            return super.name.uppercased() // 부모 클래스의 name 프로퍼티 값 대문자로 반환
        }
        set {
            super.name = newValue // 부모 클래스의 name 프로퍼티 값 변경
        }
    }
    
    // 계산 프로퍼티 재정의
    override var description: String {
        return super.description + ", Student ID: \(studentID)"
    }
}

// 객체 생성
let student = Student(name: "John", studentID: "12345")

print(student.name) // 출력: "JOHN"
print(student.description) // 출력: "Name: John, Student ID: 12345"

위의 예시에서 Person 클래스는 name 프로퍼티와 description 계산 프로퍼티를 가지고 있습니다. Student 클래스는 Person 클래스를 상속받아 studentID 프로퍼티를 추가하고 있습니다.

Student 클래스에서는 name 프로퍼티와 description 계산 프로퍼티를 재정의하고 있습니다. name 프로퍼티의 경우 대문자로 변환하여 반환하고, description 프로퍼티의 경우 학생 ID를 추가하여 반환하고 있습니다.

3) 프로퍼티 감시자

프로퍼티 감시자도 프로퍼티의 접근자와 설정자처럼 재정의할 수 있습니다. 하지만 프로퍼티의 접근자와 프로퍼티 감시자는 동시에 재정의할 수 없습니다. 만약 둘 다 동작하길 원한다면 재정의하는 접근자에 프로퍼티 감시자의 역할을 구현해야 합니다.

class ParentClass {
    var number: Int = 0 {
        willSet(newNumber) {
            print("Will set to \(newNumber)")
        }
        
        didSet {
            print("Did set to \(number)")
        }
    }
}

class ChildClass: ParentClass {
    override var number: Int {
        willSet {
            print("Child will set to \(newValue)")
        }
        
        didSet {
            if number > oldValue {
                print("Increased by \(number - oldValue)")
            } else {
                print("Decreased by \(oldValue - number)")
            }
        }
    }
}

let parentObject = ParentClass()
parentObject.number = 5
// 출력: Will set to 5
// 출력: Did set to 5

let childObject = ChildClass()
childObject.number = 10
// 출력: Child will set to 10
// 출력: Did set to 10
// 출력: Increased by 10

childObject.number = 3
// 출력: Child will set to 3
// 출력: Did set to 3
// 출력: Decreased by 7

이 예제에서 ParentClass에는 number라는 프로퍼티가 있습니다. 이 프로퍼티에는 willSet과 didSet 프로퍼티 감시자가 있습니다. 그런 다음 ChildClass는 ParentClass를 상속받습니다. ChildClass에서는 number 프로퍼티의 willSet과 didSet을 재정의하여 특정 동작을 수행하도록 합니다.

4) 서브스크립트 재정의

class Vehicle {
    var passengers: Int = 0
    
    subscript(index: Int) -> String {
        return "This vehicle can carry \(passengers) passengers."
    }
}

class Car: Vehicle {
    override subscript(index: Int) -> String {
        get {
            return "This car can carry \(passengers) passengers."
        }
        set {
            passengers = index
        }
    }
}

let vehicle = Vehicle()
print(vehicle[4]) // 출력: This vehicle can carry 0 passengers.

let car = Car()
print(car[4]) // 출력: This car can carry 0 passengers.
car[4] = 5
print(car[4]) // 출력: This car can carry 5 passengers.

이 예제에서, Vehicle 클래스에는 passengers 프로퍼티와 인덱스를 받아서 특정 문자열을 반환하는 서브스크립트가 정의되어 있습니다. Car 클래스에서는 Vehicle 클래스를 상속받고, passengers 프로퍼티를 이용하여 해당 클래스에서 서브스크립트의 동작을 재정의합니다.

Car 클래스에서는 서브스크립트의 get 접근자를 재정의하여 새로운 문자열을 반환하고, set 접근자를 재정의하여 passengers 값을 설정합니다.

이제 Car 인스턴스를 생성하여 서브스크립트를 사용하면, Vehicle 클래스와는 달리 Car 클래스에서 정의된 서브스크립트의 동작이 실행됩니다.

5) 재정의 방지

재정의(Override)를 방지하기 위해 Swift에서는 final 키워드를 사용합니다. 이 키워드를 프로퍼티, 메서드, 서브스크립트, 클래스, 함수 등의 선언 앞에 붙여주면 해당 요소를 재정의할 수 없도록 합니다.

class BaseClass {
    final func someMethod() {
        print("This method cannot be overridden.")
    }
}

class SubClass: BaseClass {
    // 오류: 'someMethod()' cannot override a final method
    // override func someMethod() {
    //     print("This method is overridden.")
    // }
}

final class FinalClass {
    func someMethod() {
        print("This method cannot be overridden.")
    }
}

// 오류: 'FinalClass' cannot inherit from final class 'FinalClass'
// class SubFinalClass: FinalClass {
// }

이 예제에서, BaseClass의 someMethod()는 final 키워드로 표시되어 있어 하위 클래스에서 재정의할 수 없습니다. 따라서 SubClass에서 someMethod()를 재정의하려고 하면 오류가 발생합니다.

또한, FinalClass는 final 키워드로 표시되어 있어 하위 클래스를 가질 수 없습니다. 따라서 SubFinalClass에서 FinalClass를 상속하려고 하면 오류가 발생합니다.

이와 같이 final 키워드를 사용하여 재정의를 방지하면 코드의 안정성과 구조를 유지하는 데 도움이 됩니다.

18.3 클래스의 이니셜라이저

클래스 이니셜라이저(Initializer)는 클래스의 인스턴스를 생성하고 초기화하는 특별한 메서드입니다. Swift에서는 이니셜라이저도 상속됩니다. 하지만 일반적으로 부모 클래스의 이니셜라이저는 자식 클래스에서 재정의되거나 오버로드되지 않습니다. 대신, 자식 클래스는 부모 클래스의 이니셜라이저를 사용하여 자체의 초기화 작업을 수행합니다.

다음은 클래스 이니셜라이저의 상속과 재정의에 관한 예시 코드입니다.

class Vehicle {
    var wheels: Int
    
    init(wheels: Int) {
        self.wheels = wheels
    }
    
    func start() {
        print("Vehicle is starting.")
    }
}

class Car: Vehicle {
    var color: String
    
    init(wheels: Int, color: String) {
        self.color = color
        // 부모 클래스의 이니셜라이저 호출
        super.init(wheels: wheels)
    }
    
    override func start() {
        print("Car is starting.")
    }
}

let car = Car(wheels: 4, color: "Red")
print("Number of wheels: \(car.wheels)") // 출력: Number of wheels: 4
print("Color: \(car.color)") // 출력: Color: Red
car.start() // 출력: Car is starting.

이 예시에서, Vehicle 클래스에는 wheels 프로퍼티와 이니셜라이저가 있습니다. Car 클래스는 Vehicle 클래스를 상속받습니다. Car 클래스에서는 새로운 color 프로퍼티를 추가하고, 부모 클래스인 Vehicle의 이니셜라이저를 호출하여 wheels 프로퍼티를 초기화합니다.

또한, Car 클래스에서는 start() 메서드를 재정의하여 자동차가 시작하는 동작을 표현합니다.

이제 Car 클래스의 인스턴스를 생성하고 이니셜라이저와 메서드를 호출할 수 있습니다. 이러한 상속 및 재정의를 통해 코드의 재사용성과 유연성을 높일 수 있습니다.

참고자료

  • 스위프트 프로그래밍 3판(지은이: 야곰, 출판사: 한빛미디어(주))
  • ChatGPT 4
profile
4년차 앱개발자입니다.

0개의 댓글