Kotlin 中类型投影的使用站点与声明站点差异

Use-site vs declaration-site difference in type projections in Kotlin

类型层次结构

open class Fruit()

open class CitrusFruit : Fruit()

class Orange : CitrusFruit()

声明站点差异

Crate 用作 Fruit 的生产者或消费者。

不变class

class Crate<T>(private val elements: MutableList<T>) {
    fun add(t: T) = elements.add(t)    // Consumer allowed
    fun last(): T = elements.last()    // Producer allowed
}

协变 classout

class Crate<out T>(private val elements: MutableList<T>) {
    fun add(t: T) = elements.add(t)    // Consumer not allowed: Error
    fun last(): T = elements.last()    // Producer allowed
}

逆变classin

class Crate<in T>(private val elements: MutableList<T>) {
    fun add(t: T) = elements.add(t)    // Consumer allowed
    fun last(): T = elements.last()    // Producer not allowed: Error
}

使用地点差异

所有这些使用点投影都是针对上面定义的不变classCrate<T>

无投影

不允许子类型化:只能将 Crate<Fruit> 分配给 Crate<Fruit>

fun main() {
    val invariantCrate: Crate<Fruit> = Crate<Fruit>(mutableListOf(Fruit(), Orange()))

    invariantCrate.add(Orange())       // Consumer allowed
    invariantCrate.last()              // Producer allowed
}

协变投影out

允许子类型化:当 CitrusFruitFruit 的子类型时,Crate<CitrusFruit> 可以分配给 Crate<Fruit>

fun main() {
    val covariantCrate: Crate<out Fruit> = Crate<CitrusFruit>(mutableListOf(Orange()))

    covariantCrate.add(Orange())       // Consumer not allowed: Error
    covariantCrate.last()              // Producer allowed
}

逆变投影in

允许子类型化:当 CitrusFruitOrange 的超类型时,Crate<CitrusFruit> 可以分配给 Crate<Orange>

fun main() {
    val contravariantCrate: Crate<in Orange> = Crate<CitrusFruit>(mutableListOf(Orange()))

    contravariantCrate.add(Orange())   // Consumer allowed
    contravariantCrate.last()          // Producer allowed: No Error?
}

问题

  1. 在给定的示例中,我对类型投影的理解和使用是否正确?

  2. 对于逆变:为什么 last()(producer) 函数在声明处不允许,但在使用处允许?编译器不应该像在声明站点示例中那样显示错误吗?也许我错过了什么?如果只允许生产者在使用地点进行逆变,那么它的用例是什么?


我更喜欢带有示例的详细答案,但我们将不胜感激。

我的猜测是声明站点和使用站点逆变之间的区别在于编译器可以静态检查声明站点,但是在使用投影时总是存在原始的、未投影的对象 运行-时间。因此,无法阻止为 in 投影创建生产者方法。

当你写:

class Crate<in T>(private val elements: MutableList<T>) {
    fun add(t: T) = elements.add(t)    // Consumer allowed
    fun last(): T = elements.last()    // Producer not allowed: Error
}

编译器可以在编译时知道 Crate<T> 上不应该存在生成 T 的方法,因此 fun last(): T 的定义无效。

但是当你写的时候:

val contravariantCrate: Crate<in Orange> = Crate<CitrusFruit>(mutableListOf(Orange()))

实际创建的是一个Crate<Any?>,因为泛型被编译器抹掉了。虽然您指定您不关心生成项目,但是通用擦除的 Crate 对象仍然存在 fun last(): Any? 方法。

人们希望投影方法是 fun last(): Nothing,以便在您尝试调用它时给您一个编译时错误。也许这是不可能的,因为需要对象存在,因此能够 return 来自 last() 方法的东西。

让我们从使用站点开始。

当你写作时

val contravariantCrate: Crate<in Orange> = ...

右侧可以是 Crate<Orange>Crate<Fruit>Crate<Any?> 等。所以基本规则是 contravariantCrate 的任何使用都应该有效,如果它有这些类型中的任何一种。

特别是对于他们所有人

contravariantCrate.last()

是合法的(类型分别为 OrangeFruitAny?)。所以它对于 Crate<in Orange> 是合法的并且类型为 Any?.

同样适用于covariantCrate;技术上调用消费者方法 允许的,只是不使用 Orange。问题是 Crate<Nothing>Crate<out Fruit>,而你做不到

val covariantCrate: Crate<Nothing> = ...
covariantCrate.add(Orange())

相反,参数类型是 FruitCitrusFruitNothing 等的最大公共子类型,即 Nothing。并且

covariantCrate.add(TODO())

确实是合法的,因为TODO()的return类型是Nothing(但会给出无法访问代码的警告)。

声明站点 inout 实际上是说 所有 用途是 in/out。所以对于逆变 class Crate<in T>,所有调用 last() return Any?。所以你应该只用那个类型声明它。