반응형
변성(variance)이란?
- 기저 타입(base type)이 같고 타입 인자(type argument)가 다른 경우 서로 어떤 관계가 있는지 설명하는 개념
- 아래의 코드에서 List는 기저 타입 / <String>, <Any>는 타입 인자이다
List<String>, List<Any>
- 단, 여기서 String은 Any의 하위 타입이지만 List<String>은 List<Any>의 하위 타입이 아니다
그렇기 때문에 다음과 같은 코드는 에러가 발생한다
fun addStringList(list: MutableList<Any>) {
list.add("text")
}
fun addNumberList() {
val numbers = mutableListOf(1, 2, 3)
// 에러 발생
addStringList(numbers)
}
- 만약 위 코드가 에러가 발생하지 않을 경우 list에 의도하지 않은 타입 인자가 add 될 수 있다.
- 즉, 겉으로 보기에는 상위-하위 타입처럼 보이지만 실제로는 서로 아무런 관계가 없는 타입을 무공변(invariant)이라고 한다
⇒ 이러한 무공변은 가변 컬렉션에서만 발생한다
그렇기 때문에 불변 컬렉션은 무공변이 일어나지 않는다
fun addNumberList() {
val numbers =mutableListOf(1, 2, 3)
// MutableList<Any> - error
addStringMutableList(numbers)
// List<Any> - success
val anyList =listOf<Any>(0, "2")
}
"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."
왜 불변 / 가변 컬렉션에 따라 공변 / 무공변이 다르게 일어날까?
- 먼저 모든 제네릭 타입은 다음과 같이 세 가지가 존재한다
- T 타입의 값을 반환하는 연산만 제공하고 T 타입의 값을 입력으로 받는 연산은 제공하지 않는 제네릭 타입 생산자 = 즉, 제네릭 타입이 생산자 역할만 할 때
- T 타입의 값을 입력으로 받기만 하고 절대 T 타입의 값을 반환하지 않는 제네릭 타입 소비자 = 제네릭 타입이 소비자 역할만 할 때
- 위 두가지에 포함되지 않는 타입
- 마지막의 경우 (즉, 생산도 소비도 아닌 타입)은 타입 안전성을 깨지 않고는 하위 타입 관계를 유지할 수 없다
- 그 이유는 위에서 언급했다시피 하위 값을 모두 넣을 수 있다면 가변 컬렉션을 활용하기 어렵기 때문이다.
예를 들면 다음과 같다
fun test() {
val stringNode = TreeNode("Hello")
// 여기서 공변성 때문에 에러 발생
val anyNode: TreeNode<Any> = stringNode
// 값을 받는 행위 자체에는 에러가 발생하지 않는다
anyNode.addChild(123)
}
그렇다면 왜 List<T> 같은 불변 타입은 공변적일까?
불변 컬렉션은 T 타읩 값을 만들어내기만 하고 소비하지 않는다.
그렇기 때문에 List<Any>로 정의했다면 그냥 Any로 돌려줄 뿐이다.
그래서 대부분의 불변 컬렉션은 공변성을 유지한다.
in, out 프로젝션으로 공변성, 반공변성 부여하기
- 위 예시에서 봤듯이 Int는 Number를 상속하고 Int의 Super class는 Number이지만 Generic에서는 상속관계가 성립하지 않는 다는 것을 알았다. = 불변성
- 일반적인 사용에선 Int는 Number를 상속한다 = 공변성
out 프로젝션
out 프로젝션을 사용하면 불변성 -> 공변성 으로 바꾸는 것이 가능하다.
class Rectangle<T: Number>(val width: T, val height: T) {
}
fun main(args: Array<String>) {
// 제네릭에 Double을 선언
val derivedClass = Rectangle<Double>(10.5, 20.5)
// 이를 제네릭에 Number를 선언한 객체에 대입하면 에러 발생
val baseClass : Rectangle<Number> = derivedClass
}
위와 같은 예시는 에러가 발생한다.
하지만, out을 사용하면 공변성으로 바꿀 수 있다.
// out을 사용하여 공변성을 갖도록 명령한다.
class Rectangle<out T: Number>(val width: T, val height: T) {
}
fun main(args: Array<String>) {
val derivedClass = Rectangle<Double>(10.5, 20.5)
// Rectangle<Double>가 Rectangle<Number>의 하위 클래스라고 인식
// 그래서 에러가 발생하지 않는다.
val baseClass : Rectangle<Number> = derivedClass
}
in 프로젝션
in은 out의 반대입니다.
즉, 두 타입의 관계를 반대로 만들 수 있습니다.
예를 들면 위에선
Rectangle<Double>가 Rectangle<Number>의 하위 클래스였지만
in 프로젝션을 사용하면
Rectangle<Number>가 Rectangle<Double>의 하위 클래스가 될 수 있습니다.
// in을 사용하여 클래스 구조를 역전시킨다.
class Rectangle<in T: Number>(val width: T, val height: T) {
}
fun main(args: Array<String>) {
val baseClass = Rectangle<Number>(10.5, 20.5)
// Number 클래스가 Double로 들어갈 수 있게 된다
val derivedClass : Rectangle<Double> = baseClass
}
이를 반공변성(Contravariance)라고 합니다.
요약
- 변성이란 - 기저 타입(base type)이 같고 타입 인자(type argument)가 다른 경우 서로 어떤 관계가 있는지 설명하는 개념
- 불변 / 가변 컬렉션에 따라 왜 공변, 무공변이 다르게 나타나는 이유는 제네릭의 역할에 따라 다르다
- in, out 프로젝션으로 공변성, 반공변성을 자유롭게 부여할 수 있다
반응형
'코틀린' 카테고리의 다른 글
안드로이트 코틀린 Reflection(리플렉션) 기초 정의 (0) | 2023.03.09 |
---|---|
코틀린에서 자주 사용하는 어노테이션(Annotation)@ 정리-1 (0) | 2023.03.06 |
코틀린 확장함수 Scope함수 apply, with, let, also, run 이란? (0) | 2023.01.16 |
안드로이드 코틀린은 같은 변수를 계속 만들면 재활용할까? (0) | 2023.01.08 |
코틀린 inline 클래스 와 함수의 정의 및 사용하는 이유, 경우 (0) | 2023.01.07 |
"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."
댓글