[Swift] Generic과 SubType

Generic Type 때문에 Type Casting 이 안된다고요? ㅠ

naljin
7 min readNov 6, 2020

문제 상황?

protocol ViewProtocol {}
class ParentModel {}
class ChildModel: ParentModel {}
class ParentView<T:ParentModel>: ViewProtocol {}
class ChildView: ParentView<ChildModel> {}
  • ChildModelParentModel을 inherit
  • ParentView의 GenericType TParentModel을 inherit
  • ChildViewParentView를 inherit. 이때 ParentView의 GenericType은 ChildModel

자 이제 ChildView를 생성해서 typeCheck를 해봅시다.

let childView = ChildView()
typeCheck(childView)
func typeCheck(_ view: ViewProtocol) {
print(view is ParentView)
print(view is ParentView<ChildModel>)
}

여러분도 같이 결과를 생각해 보아요

정답은.. 두둥

  1. view is ParentView // false
  2. view is ParentView<ChildModel> // true

제가 결과를 보고 든 생각은 이랬어요.

엥..??? 아니 왜 첫번째게 false임? ChildViewParentView를 상속하고 있으니까 true여야 하지 않나?

ParentView를 상속받을때 설정한 GenericType은 ChildModel인데.. 얘는 ParentModel 상속하고 있는 애니까 어차피 문제 없을테고???

비슷한 다른 상황을 하나 더 살펴 볼게요.

protocol BaseProtocol {}
struct BaseModel: BaseProtocol {}
struct SomeType<T> {}
var items: Array<SomeType<BaseProtocol>> = []
let item = SomeType<BaseModel>()
  • items의 타입은 SomeType<BaseProtocol>Array
  • item의 타입은 SomeType<BaseModel>
  • BaseModelBaseProtocol 을 conform

이 상황에서 아래처럼 append 한다면?

items.append(item)

이런 에러 메시지와 함께 에러가 납니다.

Cannot convert value of type 'SomeType<BaseModel>' to expected argument type 'SomeType<BaseProtocol>'

아니 BaseModelBaseProtocol 을 따르고 있는데도 에러가 난다구? 도대체 왜???

원인

이는 Swift의 Generic이 invariant 하기 때문에 생긴 일입니다.

🙋🏻‍♀️ invariant가 뭔데요?

😎 covariant하지 않다는 뜻이요!

🤦🏻‍♀ covariant는 또 뭔데요;;;

자 용어부터 짚고 갑시다.

Variance

  • T 는 element, <T> 는 element를 포함하는 컴포넌트
  • A → B 이면 A 는 B 의 서브 타입
  • 서브 타입은 상속만이 아니라 더 넓은 개념인 Subtyping 을 의미

이런 상황에서 Variance란 아래와 같이 정의할 수 있습니다.

element 간의 sub type 관계컴포넌트간sub type 관계영향을 주는 정도

그리고 영향도에 따라 다음과 같이 나뉩니다.

  • Invariant (무공변성) : T → T’ 일 때 <T>, <T’> 는 서로 별개의 타입이면 <T> 는 Invariant 하다.
  • Covariant (공변성) : T→ T’ 일 때 <T> → <T’> 가 성립하면 <T> 는 covariant 하다.
  • Contravariant (반공변성) : T → T’ 일 때 <T> ← <T’> 가 성립하면 <T> 는 contravariant 하다.

아까 Swift의 Generic이 invariant하다고 했죠? 이는 즉 TU 가 subtyping 같은 관계일지라도 어쨌든 T != U이면 X<T>X<U> 와 완전 무관하다는 의미입니다.

이렇게 <T>가 기본적으로 invariant한 이유는 타입 안정성을 컴파일 타임에 보장하기 위해서라고 합니다.

만약 Parameterized Type(<T>) 에 subtyping 이 허용된다고 가정해봅시다. 그렇게 되면 어떤 타입의 파라미터가 넘어올지 컴파일 타임에 미리 아는 것이 불가능하고, 예외 발생을 막기위해 지속적으로 타입 체크가 필요합니다. 즉, 제네릭의 장점을 전혀 취할 수 없게 됩니다.

또한 subtyping 이 허용되면 제네릭 타입과 관련된 paramter 및 setter 등을 정의하는 generic reference type을 완전히 깨트릴 수 있다..고하는데 더 자세한건 아래 출처들을 참고해주세여

마무리

T → U 인 사실과 관계없이 X<T>X<U> 는 완전 다른 것!이 Swift Generic의 특성이군요.

protocol ViewProtocol {}
class ParentModel {}
class ChildModel: ParentModel {}
class ParentView<T:ParentModel>: ViewProtocol {}
class ChildView: ParentView<ChildModel> {}
let childView = ChildView()
typeCheck(childView)
func typeCheck(_ view: ViewProtocol) {
print(view is ParentView) //false
print(view is ParentView<ChildModel>) //true
}

제가 view is ParentView 결과가 false인걸 보고 이렇게 생각했다고 했쥬?

ChildView ParentView를 상속받을때 설정한 GenericType은 ChildModel인데.. 얘는 ParentModel 상속하고 있는 애니까 어차피 문제 없을테고???

문제가 없는게 아니라 얘 땜에 문제가 되는거였네요 ㅎ

다시 한번 말하지만 Swift Generic은 Invariant하기 때문이죠~

ChildModel → ParentModel인 사실과 관계없이 ParentView<ChildModel>ParentView<ParentModel> 는 완전 다른 것!

오키오키! 그럼 행복한 주말 되시길~ 즐주행주~~! ㅃㅏ이!

출처

--

--

No responses yet