• [Kotlin in Action, Effective Kotlin] 제네릭과 변성 variance
    언어/Kotlin 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]

     

    같이 보면 좋은글 : 

    참고 :

     

     

    반응형

    댓글

Designed by Tistory.