How does nested covariance works in Kotlin?

Viewed 103

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...

1 Answers

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
}
Related