问题描述
我正在寻找一个在类声明中使用 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>
,这意味着List
在T
中是协变。
正如我们刚刚看到的那样,声明列表意味着以下内容成立:“如果 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
。