为什么在泛型中禁止使用 'out' 关键字,如果一个方法除了类型参数作为参数?

Why it is forbidden to use 'out' keyword in generics if a method excepts the type parameter as a parameter?

我正在寻找一个示例,当在 class 声明中使用 out 并且 class 有一个获取参数类型作为参数。

此外,我正在寻找一个在 class 声明中使用 in 并且参数类型为 var 时可能导致问题的示例 class 成员? 我认为我将能够仅通过示例来理解规则

关于方差的小提示

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

例如,如果 child class C 扩展 parent P 那么:

  • List<C> 是否扩展 List<P>? (List<T>T 中是协变的)
  • 还是反过来? (逆变)
  • 或者List<C>List<P>没有关系? (不变)。

例子

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

让我们假设以下 class 声明:

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 class 中声明一个接受参数 T 的方法会发生什么?

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

大多数语言(包括 Kotlin)的规则规定,如果一个方法接受类型为 T 的参数,从技术上讲,您可以获得 T 或任何子类的实例class of T(这就是 subclassing 的意义所在),但你至少拥有所有可用的 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 的子 class声明了 Child.

类型参数的方法

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

open class Animal

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

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

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 并且编译器允许我们将该类型用作 return 值:

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

那么如果你这样做,将导致编译器不可能将 return 类型转换为子类型。

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 就像一个函数 return 是一个值, var 属性 另外还有一个 setter,这就像一个带参数的函数。因此 val 属性与逆变(in)不兼容,并且 var 属性与逆变或协变不兼容(out)。私有属性不受这些限制的影响,因为在 class 的内部工作中,类型是不变的。所有 class 可以知道的关于它自己的类型的就是它的边界。方差只会影响 class 被外界投射(查看)的方式。

所以一个 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