언어/Kotlin

[Kotlin in Action, Effective Kotlin] 제네릭과 변성 variance

원석💎-dev 2022. 5. 29. 18:37
반응형

1. 공변성 (covariant)

  •  Set과 List는 공변성이 있다. String은 Any로 업캐스팅 될 수 있으므로, List<String>은 List<Any>로 업캐스팅 될 수 있다. 공변성이란 하위 타입 관계가 유지되는 것이다. List가 공변성이 있다는 것을 나타내기 위해 제네릭 E를 <out E>로 선언한다.
  • out이란, 제네릭 E가 out 위치에서 사용되어야 한다는 의미이다. out위치란 public 접근 제한자인 변수, 함수의 return 타입의 위치를 의미한다. E가 반환 타입에 사용된다면, 그 함수는 E타입의 값을 생산(Produce)한다고 정의한다.
public interface List<out E> : kotlin.collections.Collection<E> {
 .
 public operator fun get(index: Int): E
 .
 .
 public fun indexOf(element: @UnsafeVariance E): Int
 .
 .
}
  • 위 코드에서 E는 out위치에서 사용되어야한다. 그러므로 get 메서드는 제네릭 E를 out 위치에서 사용하고 있다.
  • indexOf 메서드는 제네릭 E를 왜 in 위치에서 사용할 수 있나? List내부의 요소를 추가 혹은 변경, 삭제하지 않는 함수에는 @UnsafeVariance 어노테이션으로 제네릭 E를 in 위치에서 사용할 수 있다. < 명확하지 않다면 사용하지 말자.

 

2. 무공변성 (invariant)

  • MutableSet과 MutableList는 무공변성이 있다. String은 Any로 업캐스팅 될 수 있으나, MutableList<String>은 MutableList<Any>로 업캐스팅 될 수 없다. 무공변성이란 타입 관계가 유지되지 않는 것이다. MutableList가 무공변성이 있다는 것을 나타내기 위해 제네릭 E를 <E>로 선언한다. 기본적인 제네릭은 모두 무공변성이다.
public interface MutableList<E> : List<E>, MutableCollection<E> {
.
.
    override fun add(element: E): Boolean
.
.
    public operator fun set(index: Int, element: E): E
.
.
}
  • 무공변성이 있는 제네릭 타입E는 in 위치와 out 위치 어디에서든지 사용가능하다.
fun addInteger(list: MutableList<Any>) {
   list.add(43)
}

val strings: MutableList<String> = mutableListOf("abc", "def")
addInteger(strings)  // ["abc", "def", 43]
println(strings.maxBy { it.length })
  • 만약 mutableList가 공변성이라면? 위 코드는 runtime시점에 오류가 발생한다. 이런 문제를 막기위해 mutableList는 무공변성이며, addInteger(strings)를 호출하는 시점에 타입이 일치하지 않는다는 오류가 발생한다.

 

3. 반공변성 (contravariant)

  • Comparator은 반공변성이 있다. String가 Any로 업캐스팅 될 수 있으면, Comparator<Any>가 Comparator<String>으로 업캐스팅 될 수 있다. 즉, String의 상위 객체(부모)가 Any면, Comparator<Any>의 상위객체(부모)는 Comparator<String>이다. 공변성이란 공변성과 정반대로 타입 관계가 바뀌는 것이다. Comparator가 반공변성이 있다는 것을 나타내기 위해 제네릭 T를 <in T>로 선언한다.
interface Comparator<in T> {
   fun compare(e1: T, e2: T): Int { ... }
}
  • in이란, 제네릭 T가 in 위치에서 사용되어야 한다는 의미이다. in 위치란 함수의 파라미터 타입의 위치를 의미한다. T가 파라미터 타입에 사용된다면, 그 함수는 T타입의 값을 소비(consume)한다고 정의한다.

 

4. 왜?

- 왜 무공변성이 필요할까?

  •  컴파일 시점에 타입이 일치하지 않는 문제가 있어 아이템이 변경 및 추가 되는 MutableXXX는 무공변성을 가진다.

- 왜 공변성이 필요할까? 

  • Int는 Number로 업캐스팅 되므로, Int는 자식이고 Number는 부모 클래스다. 공변성 상황에서, 마찬가지로 List<Int>는 자식이고 List<Number>는 부모 클래스다. 부모 자식 관계에서 부모의 함수는 자식 클래스에서 사용이 가능하다.
  • 아래 코드에서 addNumberToList 함수는 List<Number>의 확장함수이다. 자식 클래스에서 부모 클래스의 함수는 사용이 가능하므로 아래 List<Int>와 List<Double>에서도 사용이 가능하다.
fun List<Number>.addNumberToList(number: Int): List<Number> = this.map { it.toDouble + number }

val integers: List<Int> = listOf(1, 2, 3, 4)
val doubles: List<Double> = listOf(5.0, 6.0, 7.0, 8.0)

println(integers.addNumberToList(10)) // [11.0, 12.0, 13.0, 14.0]
println(doubles.addNumberToList(10)) // [15.0, 16.0, 17.0, 18.0]
  • 반대로 무공변성인 MutableList에 확장함수를 만들면 어떻게 될까? 아래 코드에서 MutableList<Number>는 MutableList<Int>, MutableList<Double>과 연관관계가 없으므로, 확장함수를 사용할 수 없다.
fun MutableList<Number>.addNumberToList(number: Int): List<Number> = this.map { it.toDouble + number }

val integers: List<Int> = listOf(1, 2, 3, 4)
val doubles: List<Double> = listOf(5.0, 6.0, 7.0, 8.0)
val numbers: List<Number> = listOf(5.0, 6.0, 7.0, 8.0)

println(integers.addNumberToList(10)) // compile error
println(doubles.addNumberToList(10)) // compile error
println(numbers.addNumberToList(10)) // [15.0, 16.0, 17.0, 18.0]

 

같이 보면 좋은글 : 

참고 :

 

 

반응형