Search code examples
kotlingenericscovariance

How does nested covariance works in Kotlin?


Here's a code in which I'm having hard time understanding why the first compiles and second doesn't?

class Test11<T : Number> {
    lateinit var test: MutableList<out T>.() -> Unit
}

fun main() {
    val test: Test11<Int> = Test11<Int>()
    val test2: Test11<out Number> = test
    test.test.invoke(MutableList(3) { 55 })  // First
    test2.test.invoke(MutableList(3) { 55 })  // Second
}

The second says MutableList<Nothing> was expected.

So basically in first case, T => Int so out out T => out Int => out Number maybe. In second case, T => out Number so anything which is subclass of Number, then still out T => out Number right?

I'm not able to understand why doesn't it work by that logic...


Solution

  • The MutableList is a function parameter. You'd have the exact same issue with:

    class Test11<T : Number> {
        fun test(list: MutableList<out T>) {
    
        }
    }
    
    fun main() {
        val test: Test11<Number> = Test11<Number>()
        val test2: Test11<out Number> = test
        test.test(MutableList(3) { 55 })  // First
        test2.test(MutableList(3) { 55 })  // Second
    }
    

    A covariant type by definition prevents functions where the type is a parameter from being called, but this also logically extends to nested covariance of the same type. If T is covariant (for the class), then it is not any more safe to consume an object that can produce Ts than to consume Ts directly.

    Example of how this could create a failure:

    class Test11<T : Number> {
        var list: MutableList<out T>? = null
        fun test(list: MutableList<out T>) {
            this.list = list
        }
    }
    
    fun main() {
        val test: Test11<Long> = Test11()
        val test2: Test11<out Number> = test
        val doubleList: MutableList<out Number> = mutableListOf(1.0)
        test2.test(doubleList) // Not allowed
        
        // if it were allowed:
        val long: Long? = test.list?.firstOrNull() // ClassCastException casting the Double to a Long
    }