如果方法将类型参数作为参数除外,为什么禁止在泛型中使用 'out' 关键字? 关于差异的小提醒示例

问题描述

我正在寻找一个在类声明中使用 out 时可能会导致问题的示例,并且该类有一个将参数类型作为参数获取方法

另外,我正在寻找一个在类声明中使用 in 并且参数类型是类的 var 成员时可能导致问题的示例? 我想我只能通过例子来理解规则

解决方法

假设这些是我们正在使用的类:

open class Animal

class Cat: Animal() {
    fun meow() = println("meow")
}

如果我们使用协变 out 类型创建这样的类,并且编译器允许我们使用该类型作为函数参数:

class Foo<out T: Animal> {
    private var animal: T? = null

    fun consumeValue(x: T) { // NOT ALLOWED
        animal = x
    }

    fun produceValue(): T? {
        return animal
    }
}

如果你这样做,就会导致我们试图在没有 meow 函数的 Animal 上调用 meow 的不可能的情况:

val catConsumer = Foo<Cat>()
val animalConsumer: Foo<Animal> = catConsumer // upcasting is valid for covariant type
animalConsumer.consumeValue(Animal())
catConsumer.produceValue()?.meow() // can't call `meow` on plain Animal

如果我们使用逆变 in 类型创建这样的类,并且编译器允许我们使用该类型作为返回值:

class Bar<in T: Animal>(private val library: List<T>) {
    fun produceValue(): T  { // NOT ALLOWED
        return library.random()
    }
}

如果你这样做,它会导致编译器不可能将返回类型转换为子类型。

val animalProducer: Bar<Animal> = Bar(List(5) { Animal() })
val catProducer: Bar<Cat> = animalProducer // downcasting is valid for contravariant type
catProducer.produceValue().meow() // can't call `meow` on plain Animal

一个属性有一个getter,就像一个返回值的函数,一个var属性还有一个setter,就像一个带参数的函数。因此,val 属性与逆变(in)不兼容,var 属性与逆变或协方差(out)不兼容。私有属性不受这些限制的影响,因为在类的内部工作中,类型是不变的。类所能知道的关于它自己类型的所有信息就是它的边界。差异只影响外部世界如何投射(查看)类。

所以一个带有 val 的例子足以说明为什么任何属性都与逆变不兼容。您可以将下面的 val 替换为 var,也没有什么不同。

class Bar<in T: Animal>(
    val animal: T // NOT ALLOWED
)

val animalProducer: Bar<Animal> = Bar(Animal())
val catProducer: Bar<Cat> = animalProducer // downcasting is valid for contravariant type
catProducer.animal.meow() // can't call `meow` on plain Animal
,

关于差异的小提醒

当你有一个泛型类 G<T>(参数化类型)时,差异是关于定义不同 G<T> 的类型 T 的层次结构之间的关系,以及不同的 T 本身。

例如,如果子类 C 扩展父类 P,则:

  • List<C> 是否扩展 List<P>? (List<T> 将在 T 中协变)
  • 还是相反? (逆变)
  • 还是List<C>List<P>之间没有关系? (不变)。

示例

现在,考虑List<out T>,这意味着ListT 中是协变。 正如我们刚刚看到的那样,声明列表意味着以下内容成立:“如果 C 扩展 P,则 List<C> 扩展 List<P>”。

让我们在这里假设以下类声明:

open class Parent {
    fun doParentStuff()
}

class Child : Parent() {
    fun doChildStuff()
}

List<out T> 的协方差意味着这是可能的:

val listOfChild: List<Child> = listOf<Child>(Child(),Child())
// this is ok because List is covariant in T (out T)
// so List<Child> is a subtype of List<Parent>,and can be assigned to listOfParent
val listOfParent: List<Parent> = listOfChild 

那么如果我们可以在 List 类中声明一个接受参数 T 的方法会发生什么?

class List<out T> {
    fun add(element: T) {
        // I can guarantee here that I have an instance of T,right?
    }
}

大多数语言(包括 Kotlin)的规则规定,如果一个方法接受类型为 T 的参数,从技术上讲,您可以获得 T 的实例 或 T 的任何子类(这是子类化的重点),但您至少可以使用 T 的所有 API。

但请记住,我们声明了 List<out T>,这意味着我可以:

val listOfChild: List<Child> = listOf<Child>(Child(),Child())
// this is ok because List is covariant in T (out T)
val listOfParent: List<Parent> = listOfChild

// listOfChild and listOfParent point to the same list instance
// so here we are effectively adding a Parent instance to the listOfChild
listOfParent.add(Parent()) 

// oops,the last one is not an instance of Child,bad things will happen here
// we could fail right here at runtime because Parent cannot be cast to Child
val child: Child = listOfChild.last

// even worse,look at what looks possible,but is not:
child.doChildThing()

在这里你可以看到,从 List<Child> 实例中,我们实际上可以接收到一个 Parent 的实例,它不是 Child 的子类,在一个声明了参数的方法中输入 Child