🐦 Swift

[Swift] 클래스의 상속, 생성자, 타입캐스팅

dev_zoe 2023. 6. 11. 19:02
반응형

본 포스팅은 '스위프트 프로그래밍 (3판)' 도서 앨런 Swift 문법 마스터스쿨 강의를 참고하여

Swift 프로그래밍에 대해 정리하는 글입니다.

혹시 틀린 부분이 있거나 질문이 있으시다면 언제든지 댓글 달아주시면 정말 감사하겠습니다 :)


클래스는 메소드나 프로퍼티 등을 다른 클래스로부터 상속받을 수 있다. (구조체는 X)

이 때 속성을 물려주는 클래스를 부모/상위/슈퍼 클래스라고 하며, 물려받는 클래스를 자식/하위/서브 클래스라고 한다.

그리고 어떠한 클래스를 물려 받지 않은 클래스를 기본(Base) 클래스 라고 한다.

 

클래스의 상속

- 클래스 상속하는 방법: 옆에 상속받을 부모 클래스 이름을 명시해주면 된다.

class 자식 클래스이름: 부모 클래스 이름 {

}

 

- 클래스가 어떤 클래스를 상속하면, 부모 클래스의 프로퍼티와 메소드를 별도로 정의하지 않아도 자동으로 물려받으며, 여기서 프로퍼티나 메소드를 추가하거나 재정의할 수 있다.(override)

class Person {
    var name: String = ""   // 저장 프로퍼티
    var age: Int = 0
    
    var greet: String {     // 연산 프로퍼티(읽기 전용)
        get {
            return "나의 이름은 \(name)이고, 나이는 \(age)입니다. 반갑습니다!"
        }
    }
    
    func today() {     // 메서드
        print("오늘도 힘찬 하루를 보냅니다!")
    }
}

class Student: Person {
//  var name: String = ""
//  var age: Int = 0    // 정의되지 않아도 자동으로 물려받음
    var major: String = ""  // 새로운 프로퍼티 추가
    
    func study() {   // 새로운 메서드 추가
        print("열심히 공부합니다.")
    }
}

let personLee: Person = Person()
personLee.name = "이철수"
personLee.age = 50
print(personLee.greet)  //나의 이름은 이철수이고, 나이는 50입니다. 반갑습니다!
personLee.today()       //오늘도 힘찬 하루를 보냅니다!

let personKim: Student = Student()
personKim.name = "김영희"
personKim.age = 20
personKim.major = "Computer Science"
print(personKim.greet)  //나의 이름은 김영희이고, 나이는 20입니다. 반갑습니다!
personKim.today()       //오늘도 힘찬 하루를 보냅니다!
personKim.study()       //열심히 공부합니다.

- 같은 기능을 하는 메소드를 여러번 정의할 필요가 없어 코드의 가독성을 높여준다.

 

재정의 (override) 규칙

1) 저장 속성

- 같은 저장속성으로 재정의는 불가하다.

- 단 저장속성 -> 계산 속성(읽기 쓰기 모두 재정의해야함)/프로퍼티 감시자 등을 통한 재정의는 가능 (2가지 속성은 실질적 메서드이기 때문에. 단순 메서드 추가는 가능함)

 

2) 메서드

- 메서드는 어떠한 형태이든 자유롭게 재정의가 가능하다.

 

3) 계산 속성

- 읽기/쓰기 -> 읽기 불가 읽기 -> 읽기 / 쓰기 형태로 확장하는 재정의 가능

(읽기 전용 계산 프로퍼티에 속성 감시자 재정의 불가능 -> 어차피 읽기만 가능한데 값의 변화가 없으므로 논리적으로 성립이 안됨)

- 속성 감시자 (읽기/쓰기) 추가하는 방식의 재정의 가능

(단, 원래는 계산 속성과 속성 감시자를 같이 사용 못하게 되어있는데 재정의할 경우에 예외적으로 허용)

 

class Vehicle {
    var currentSpeed = 0.0  // 저장속성

    var halfSpeed: Double { // 계산 속성
        get {
            return currentSpeed / 2
        }
        set {
            currentSpeed = newValue * 2
        }
    }
}



class Bicycle: Vehicle {
    // 저장 속성 추가는 당연히 가능
    var hasBasket = false
    
    // 1) 저장속성 ===> 계산속성으로 재정의(메서드 추가) 가능
    // 저장 속성을 읽기 속성으로만 하면 재정의 불가 (읽기/쓰기 둘다 구현해야함)
    override var currentSpeed: Double {
        // 상위 속성이기 때문에 super키워드 필요
        get {
            return super.currentSpeed       // 1.0
        }
        set {
            super.currentSpeed = newValue
        }
    }

    // 1) 저장속성 ===> 속성감시자를 추가하는 재정의(메서드 추가)는 가능
    override var currentSpeed: Double {
        // 상위 속성이기 때문에 super키워드 필요
        willSet {
            print("값이 \(currentSpeed)에서 \(newValue)로 변경 예정")
        }
        didSet {
            print("값이 \(oldValue)에서 \(currentSpeed)로 변경 예정")
        }
    }
    
    //  2) 계산속성 -> 읽기/쓰기 형태로 재정의 가능 (읽기/쓰기 -> 읽기 는 불가)
    override var halfSpeed: Double {
        get {
            return super.currentSpeed / 2
        }
        set {
            super.currentSpeed = newValue * 2
        }
    }
    
    //  2) 계산속성 -> 속성감시자 추가 가능 (예외적으로 허용)
    override var halfSpeed: Double {
        willSet {
            print("값이 \(halfSpeed)에서 \(newValue)로 변경 예정")
        }
        didSet {
            print("값이 \(oldValue)에서 \(halfSpeed)로 변경 예정")
        }
    }
}

 

4) 서브 스크립트

- 서브 스크립트도 메소드이기 때문에 재정의가 가능하다.

class School {
	var students: [Student] = []
    
    subscript(_ number: Int) -> Student {
    	print("\(number+1) 번째 학생")
        return students[number]
    }
}

class MiddleSchool: School {
	override subscript(_ number: Int) -> Student {
    	print("\(number+1) 번째 중학생")
        return students[number]
    }
}

 

재정의 방지 - final 키워드

자식 클래스에서 부모 클래스의 몇몇 특성을 재정의할 수 없도록 제한하고 싶다면, 제한하고 싶은 특성 앞에 final 키워드를 명시하면 된다.

이게 클래스의 성능상의 이점을 준다고 하는데, dynamic dispatch 이러한 개념이 나와서 그 뒤에 더 자세히 다뤄볼 예정이다.

 

생성자 (initializer)와 초기화

1) 지정 생성자 (Designated initializer)

init() {

}

- 모든 클래스는 반드시 1개 이상의 지정 생성자를 가진다. (모든 저장속성에 기본값이 있는 경우 호출하지 않아도 자동 구현 되는것임)

why? 클래스의 인스턴스를 생성하려면 반드시 저장 속성의 값을 가지고 있어야하기 때문에, 지정 생성자의 가장 중요한 역할은 저장 속성의 값을 초기화하여 인스턴스를 생성하는 일임

- 오버 로딩을 지원한다.

class Color {
    let red: Double
    let green: Double
    let blue: Double
    
    
    // 생성자도 오버로딩(Overloading)을 지원 (파리미터의 수, 아규먼트 레이블, 자료형으로 구분)
    
    init() {      // "init()" -> 기본 생성자. 저장 속성의 기본값을 설정하면 호출하지 않아도 자동 구현
        red = 0.0
        green = 0.0
        blue = 0.0
    }

    init(white: Double) {  // 오버로딩
        red   = white
        green = white
        blue  = white
    }

    init(red: Double, green: Double, blue: Double) {
        self.red   = red
        self.green = green
        self.blue  = blue
    }
}

var color = Color(white: 1.0)          // 다양한 파라미터를 통해 인스턴스 초기화
color = Color(red: 1.0, green: 2.0, blue: 2.0)

 

- struct는 프로퍼티의 기본값이 모두 구현되어 있음에도 모든 프로퍼티의 값을 저장하여 초기화 할 수 있는 "멤버와이즈 이니셜라이저"를 제공하여 생성자를 구현하지 않아도 자동 제공됨

(얘는 class에는 없음. class는 기본값이 모두 구현되어있으면 init() 만 자동구현됨)

struct Color {
    var red: Double = 1.0
    var green: Double = 1.0
    var blue: Double = 2.0

}

var color = Color()
color = Color(red: 1.0, green: 1.0, blue: 1.0)   // -> 생성자를 구현하지 않아도 자동으로 제공함

 

2) 편의 생성자 (Convenience initializer)

convenience init() {
	self.init()
}

- 적은 개수의 파라미터로 보다 편리하게 생성하기 위한 서브 개념의 생성자(필수 X)로, 지정 생성자를 반드시 호출하므로 이에 의존한다.

- 편의 생성자는 하위 클래스에서 재정의가 불가능하다. (이유는 아래에서 나옴)

 

❓ 편의 생성자는 어쩔때 쓰는지? 혹은 어쩔때 유용한가?

일부 프로퍼티만 주고 전체 저장 속성을 한꺼번에 편리하게 초기화하고 싶을 때, 지정 생성자에 중복되는 코드를 줄이고 싶을 때

class Color {
    let red, green, blue: Double

    convenience init(white: Double) {  // 지정 생성자 호출해서 초기화
        self.init(red: white, green: white, blue: white)
    }
    
    init(red: Double, green: Double, blue: Double) { // 지정 생상자
        self.red   = red
        self.green = green
        self.blue  = blue
    }
}

- 유의해야할 점은, 편의 생성자가 직접 저장 속성을 세팅해주는게 아니라 지정 생성자를 호출하는 역할만 하는 것임.

즉, 지정 생성자를 궁극적으로 호출해서 지정 생성자가 대신 저장 속성을 세팅할 수 있도록 하는 역할만 함

 

상속 관계에서의 생성자 위임 규칙

출처: https://docs.swift.org/swift-book/documentation/the-swift-programming-language/initialization/

이 그림을 잘 기억하고, 이 그림만 위반하지 않으면 모든 생성자 상속과 위임이 가능하다.

 

1) 하위 클래스의 지정생성자는 상위 클래스의 지정생성자를 반드시 호출해야한다. (왜냐 모든 저장속성이 반드시 초기화가 되어야만 인스턴스 생성이 되니까. 근데 상위 클래스의 저장속성은 하위 클래스가 아니라 상위 클래스 본인이 초기화하는것임 -> 위임)

2) 편의 생성자는 동일한 클래스 내의 다른 편의 생성자 혹은 지정생성자를 호출해야한다. (상위 클래스 지정생성자 호출 절대 불가)

3) 편의 생성자는 본인이 직접 호출하든 타고타고 호출하든 결국에는 지정 생성자를 반드시 호출해야한다. (지정 생성자만이 저장 속성을 초기화함)

 

상속 관계에 있을 때 2단계 초기화

스위프트에서 클래스 초기화는 2단계를 거친다.

1) 1단계 - 저장 프로퍼티를 먼저 초기화 함 (필수)

출처: https://docs.swift.org/swift-book/documentation/the-swift-programming-language/initialization/

2) 2단계 - 모두 초기화해야만 비로소 커스텀 혹은 메서드 실행 (선택)

출처: https://docs.swift.org/swift-book/documentation/the-swift-programming-language/initialization/

 

class Aclass {
    var x: Int
    var y: Int
    
    init(x: Int, y: Int) {    // 지정생성자
        self.x = x
        self.y = y
    }
    
    convenience init() {     // 편의생성자
        self.init(x: 0, y: 0)
    }
}

class Bclass: Aclass {
    
    var z: Int // 저장 생성자 추가 
    
    // 오버로딩
    init(x: Int, y: Int, z: Int) {    // 실제 메모리에 초기화 되는 시점
        self.z = z                 // 1단계 - 자기 자신 저장속성 먼저 초기화 
        super.init(x: x, y: y)     // 1단계 - 상위의 지정생성자 호출 -> 상위 클래스 저장속성만 상위 클래스에 만들어주기를 부탁함 
        self.doSomething() // 2단계 - 인스턴스가 위에서 생겼기 떄문에, 메소드 호출이 가능한 것임
    }
    
    convenience init(z: Int) {
        //self.z = 7     //==========> self에 접근불가 (아직 초기화를 안했으니)
        self.init(x: 0, y: 0, z: z)
        self.z = 7       // (선택) 2단계 -> 위에서 이미 인스턴스를 만들었기 때문에 커스텀이 가능함
    }
    
    convenience init() {
        self.init(z: 0)
    }
    
    func doSomething() {
        print("Do something")
    }
}

 

지정생성자 / 편의생성자 상속과 재정의 규칙

1단계

 

- 기본적으로는 지정생성자는 상속되지 않고 재정의가 필수 (개발자의 방지하여 저장 속성을 올바르게 초기화하기 위함)

- 하위 클래스에서 상위 클래스의 지정생성자를 지정/편의 생성자로 재정의가 가능하며, 예외 적으로 안전하게 모든 저장 속성이 초기화가 될 수 있는 상황이 된다면 꼭 재정의 하지 않아도 자동상속 되는 경우가 존재

- 편의 생성자는 어떠한 상황에서도 재정의가 불가함

 

2단계

 

- 현재 클래스의 모든 저장 속성 초기화 후 (기본값 혹은 현재 단계의 지정 생성자 구현) -> 상위의 지정 생성자 호출

- 편의 생성자 구현 시, 지정 생성자 호출

 

예외적으로 생성자가 자동 상속되는 경우 - 안전하게 모든 저장 속성이 초기화되는 것이 보장되는 경우

 

1) 지정생성자가 자동 상속되는 경우

- 모든 저장 속성의 기본값이 설정되어있고, 어떠한 재정의도 하지 않은 경우

2) 편의생성자가 자동 상속되는 경우

- 상위 클래스의 지정 생성자를 자동으로 상속받았거나

- 상위클래스의 지정 생성자를 모두 재정의한 경우

 

필수 생성자 (required initializer)

required init() {

}

required 키워드를 붙이면, 이 클래스를 상속받은 자식 클래스는 반드시 해당 이니셜라이저를 구현해주어야 함

단 재정의를 할 때, override 를 붙이지 않고 required 를 붙이면 된다.

 

1) 다른 지정 생성자를 구현하지 않았다면 상위 클래스에서 자동 상속된다.

2) 지정 생성자를 구현했다면, 자동 상속을 벗어나기 때문에 required init을 반드시 구현해야한다.

(지정 생성자를 따로 구현했다는 것은 새로운 저장 속성이 생겼을 가능성이 있기에 필수 생성자로 대응 하라는 의미로 받아들일 수 있음)

 

⭐️ 예시 (UIView)

- UIView를 상속해서 커스텀 뷰를 만들 때, required init을 반드시 구현해야함

class MyInfoView: UIView {
    init() {
        super.init(frame: .zero)
        setupViews()
        initialLayout()
        reset()
    }

    @available(*, unavailable)
    required init?(coder _: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

 

실패가능 생성자 (Failable initializer)

init 뒤에 ? 를 붙인 형태

init?(){
}

실패 가능성을 지닌 생성자로, 생성 실패 시 nil을 리턴하는 생성자이다.

특정 값으로 저장속성을 초기화하여 생성하고자 할 때 실패하도록 해서 개발자가 저장 속성을 완전하게 초기화할 수 있도록 도와주는 역할을 한다.

 

1) init -> init?  호출/재정의 불가능

2) init? -> init  호출/재정의 가능

 

소멸자, 디이니셜라이저 (Deinitializer)

deinit {

}

인스턴스가 메모리에서 제거되기 전에 자동으로 호출되는 메소드이며, 개발자가 정의할 때에는 인스턴스를 해제하기 전에 필요한 내용을 정의한다.

클래스에만 존재하는 개념이며, 최대 1개의 소멸자가 존재한다.

 

타입캐스팅

is 연산자

타입을 체크하여 bool 값(true/false)를 반환하는 연산자로, 하위 클래스 is 상위클래스는 true / 상위 클래스 is 하위클래스는 false 이다.

(왜냐? 하위클래스는 반드시 상위클래스의 저장속성을 물려받기 때문이고, 상위클래스는 하위클래스의 저장 속성을 가지고있지 않기 때문)

 

class Coffee {
    let shot: Int
    
    init(shot: Int) {
        self.shot = shot
    }
    
    func coffee() {
    	print("우와 커피다!")
    }
}

class Latte: Coffee {
    var flavor: String // 저장속성 추가
    
    init(flavor: String, shot: Int) {
        self.flavor = flavor  // 자기자신의 저장속성을 먼저 초기화해야함
        super.init(shot: shot) // 그리고 나서 상위 클래스의 생성자 호출
    }
    
    func latte() {
    	print("우와 라떼다!")
    }
}

class Americano: Coffee {
    let iced: Bool
    
    init(shot: Int, iced: Bool) {
        self.iced = iced
        super.init(shot: shot)
    }
    
    func americano() {
    	print("우와 아메리카노다!")
    }
}
 
let coffee: Coffee = Coffee(shot: 1)
let latte: Latte = Latte(flavor: "plain", shot: 2)
let icedAmericano: Americano = Americano(shot: 2, iced: true)

print(coffee is Coffee) // true
print(coffee is Americano) // false

print(latte is icedAmericano) // false

 

as 연산자 / 업캐스팅, 다운캐스팅

업캐스팅: 하위 클래스가 상위 클래스의 타입으로 캐스팅 하는것 -> as : 항상 성공

다운캐스팅: 상위 클래스가 하위 클래스의 타입으로 캐스팅 하는것 -> as? as! : 실패의 가능성이 있음

(왜냐? 하위 클래스가 상위 클래스의 모든 저장속성을 항상 잘 초기화하고 있다는 보장이 없기 때문)

let coffe: Coffee = Latte(flavor: "vanilla", shot: 2)   // 업캐스팅
if let firstCoffee: Americano = coffee as? Americano {   // 다운캐스팅

}

if let secondCoffee: Latte = coffee as? Latte {

}

 

여기서 조금 특이?유의 해야할 사항은 업캐스팅 했을 때 인스턴스를 하위 클래스로 만든다고 하더라도 타입은 상위 클래스면 항상 상위 클래스의 저장속성이나 메소드만 접근 가능하다는 점이다.

let coffee: Coffee = Americano()
coffee.americano() /// 불가능!
coffee.coffee()    // 우와 커피다!

여기서는 Americano() 로 인스턴스를 생성했으나 타입은 상위 클래스인 Coffee 이기 때문에, coffee.americano() 메소드 호출이 불가하다.

 

Any / AnyObject 타입

Any: 어떠한 데이터 타입의 인스턴스도 다 표현할 수 있는 타입

AnyObject: 어떠한 클래스의 인스턴스도 다 표현할 수 있는 타입

var some: Any = "Swift"

//some.count  -> 문자열임에도 안되는 이유? Any 타입으로 인식하기 때문에 count 인지 X

(some as! String).count  // String으로 다운캐스팅하여 사용

some = 10
some = 3.2

let array: [Any] = [5, "안녕", 3.5, Person(), Superman()]
let objArray: [AnyObject] = [Person(), Superman(), NSString()]

//objArray[0].name
(objArray[0] as! Person).name
for item in array {
    switch item {
    case is Int:                                  // item is Int
        print("Index - \(index): 정수입니다.")
    case let num as Double:                      // let num = item as Double -> 사용하려면 반드시 타입캐스팅을 해주어야함
        print("Index - \(index): 소수 \(num)입니다.")
    case is String:                               // item is String
        print("Index - \(index): 문자열입니다.")
    case let person as Person:                    // let person = item as Person
        print("사람입니다.")
        print("이름은 \(person.name)입니다.")
        print("나이는 \(person.age)입니다.")
    default:
        print("그 이외의 타입입니다.")
    }
}

 

*Any vs AnyObject 차이

1) Any는 타입이고, AnyObject는 프로토콜임 (프로토콜에서 더 다룸)

2) Any는 어떠한 타입도 올 수 있음을 나타내는 타입이고, AnyObject는 어떠한 클래스 인스턴스도 올 수 있음을 나타낸다.

반응형