Swift 상속 클래스에서 저장 속성 오류 및 생성자 순서 다루기

나우리·2024년 5월 11일

Swift

목록 보기
2/13

Hacking with Swift의 Swift 100Days Challenge checkpoint7 번은 확실히 이전보다는 어려웠다.

Java의 extends class를 이미 알고 있어서 클래스를 상속하여 확장 및 분리하여 활용할 수 있다는 개념은 이미 알고 있었지만,

역시 직접 작성하는 건 다른 듯 했다. Swift의 초기화 개념을 통해 클래스를 8개 만들게 되니 복잡하게 느껴졌다.

특히 작성하면서 두 개의 에러와 마주쳤는데 이 점이 어렵게 느껴졌다.

Cannot override with a stored property 'property_name'

class Mother {
    var property_name : String
}

class daughter : Mother {
    override var property_name : String = "name"
}

일단 오버라이드 func은 가능하지만 오버라이드 속성값은 불가능하다.
정해진 값의 경우 상속 클래스 및 인스턴스 전반에서 하나로 통용되기에 결국 하나의 클래스에서만 저장되어야 혼란이 적기 때문인 듯하다.
그렇지만 부모 클래스에서는 이랬지만 아래 클래스에서는 전반적인 내용이 바뀌었으면 좋겠다면,
보통은 계속 바뀌거나 사라져도 되는 내용을 function으로 따로 작성을 하는 듯하지만 꼼수는 확실히 있다.

"스택오버플로우 : stored property 오버라이딩"

이 스택오버플로우 게시물을 참조한다면, 계산 프로퍼티(computed property)를 사용할 경우에는 쉽게 바꿀 수 있다고 하지만 이 경우는 지금 내가 사용하는 엑스코드에서는 적용되지 않았다. (15.3을 사용하고 있다)

그 대신 init을 통해서 초기화값을 넣어주는 식으로 활용은 가능했다.
형태만 지정된 것들을 재정의하는 게 아니라 Init에서 값을 넣어서 바꿔줄 수 있었고,
나는 손자클래스 (조부모 - 부모 - 손자 형태)까지 신경써야 했기 때문에 값을 아예 init에서 지정해주기보다는
함수의 변수 입력에 기본값을 미리 지정해주는 형식으로 진행했다.

class Dog : Animal {
    func speak() {
        print("\(self.sound)")
    }

    override init(_ legs: Int = 4, _ sound: String = "멍멍") {
        super.init(legs, sound)
    }
}

class Corgi : Dog {
    init() {
        super.init(4, "월월")
    }
}

이렇게 해두면 인스턴스를 생성할 때 Dog()로 호출해도 기본값으로 만들어진다.
그리고 어디까지나 기본값이기 때문에 하위 클래스의 init에서 프로퍼티 초기값을 변경할 수 있다.

Property 'self.property_name' not initialized at super.init call

class Animal {
    var legs : Int
    var sound : String

    init(_ legs: Int, _ sound : String) {
        self.legs = legs
        self.sound = sound
    }
}

class Cat : Animal {
    init(_ isTame: Bool, _ sound : String = "야옹") {
        super.init(4, sound)
        self.isTame = isTame
    }
    var isTame : Bool

    func speak() {
        print("\(self.sound)")
    }

}

다음과 같은 코드를 작성하면 super.init에 빨간 줄이 생성되면 에러메시지가 나온다.

이 문제는 swift의 property 초기화가 두 단계로 진행되기 때문에 나타나는 에러메시지로 단순히 self를 먼저 할당하면 해결된다.

Swift가 두 단계로 초기화를 진행하기 떄문이다.
1 단계에서는 현재 이니셜라이저가 시작된 클래스에 저장된 속성이 모두 초깃값을 갖게 한다.
2 단계에서는 각 클래스가 저장된 속성값들을 변경할 기회를 준다. 부모 클래스의 값들은 이 때 변경되는 것이다.

이는 속성값들이 각기 다른 생성자로 예상치않게 다르게 정의되는 것을 막는다.
예를 들어 override된 함수나 기존에 저장된 것에서 값이 달라진 속성이 있다고 하면 (사실 swift에서 속성 변경을 강하게 막고 있어서 이런 일이 있을지 모르겠지만) 현 클래스의 생성자를 먼저 실행하여 이 문제를 해결하는 것이다.

Java 생성자에서는 그냥 부모 클래스를 냅다 호출한 뒤 자식 클래스를 다시 호출해서 오버랩했던 느낌인데,
이 점에서는 Swift가 훨씬 잘못된 활용들을 신경쓰고 있다고 느꼈다.

Full Code of checkpoint7


import Foundation


//make a class hierarchy for animals, starting with Animal at the top, then Dog and Cat as subclasses, then Corgi and Poodle as subclasses of Dog, and Persian and Lion as subclasses of Cat.

class Animal {
    //let으로 설정 시 한 번 설정 후 바꿀 수 없는 값인데, 물론 최악의 케이스이지만 개별 인스턴스 별로 몸이 불편할 수 있다고 생각함 (변할 수 있다)
    var legs : Int
    var sound : String

    init(_ legs: Int, _ sound : String) {
        self.legs = legs
        self.sound = sound
    }
}

class Dog : Animal {
    func speak() {
        print("\(self.sound)")
    }

    override init(_ legs: Int = 4, _ sound: String = "멍멍") {
        super.init(legs, sound)
    }
}

class Cat : Animal {
    init(_ isTame: Bool, _ sound : String = "야옹") {
        self.isTame = isTame
        super.init(4, sound)
    }
    var isTame : Bool

    func speak() {
        print("\(self.sound)")
    }

}

class Corgi : Dog {
    init() {
        super.init(4, "월월")
    }
}

class Poodle : Dog {
    init() {
        super.init(4, "올오오오오")
    }
}

class Persian : Cat {
    init(_ isTame: Bool) {
        super.init(isTame, "페르시앙")
    }
}

class Lion : Cat {
    init(_ isTame: Bool) {
        super.init(isTame, "어흥")
    }
}

var corgi = Corgi()
corgi.speak()

var persian = Persian(true)
persian.speak()
persian.isTame = false

var dog = Dog()
dog.speak()

사실 힌트 부분을 보면 그냥 func speak()에서 출력값만 오버라이딩해주는 방식을 말한 것 같았는데,
그냥 저장값이 있어야한다고 생각하고 sound를 지정해서 이렇게 오래 돌아온 것 같다.
그렇지만 요 방식으로 배운 점이 있어서 이것도 나쁘진 않았던 것 같다.
게다가 이 방식을 통해서 speak값도 나중에 변경 가능하다...!

참조

Swift 공식문서(5.10): 2단계 생성자

profile
왕초보 개발일지

0개의 댓글