【发布时间】:2021-04-22 20:25:20
【问题描述】:
我正在寻找一个在类声明中使用 out 时可能导致问题的示例,并且该类有一个将参数类型作为参数的方法。
另外我正在寻找一个在类声明中使用 in 并且参数类型是类的 var 成员时可能导致问题的示例? 我想我只能通过例子来理解规则
【问题讨论】:
标签: kotlin generics covariance contravariance
我正在寻找一个在类声明中使用 out 时可能导致问题的示例,并且该类有一个将参数类型作为参数的方法。
另外我正在寻找一个在类声明中使用 in 并且参数类型是类的 var 成员时可能导致问题的示例? 我想我只能通过例子来理解规则
【问题讨论】:
标签: kotlin generics covariance contravariance
假设这些是我们正在使用的类:
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
【讨论】:
Animal 和Cat 比Child 和Parent 做出了更好的例子:D
class Foo<out T: Cat> { fun consumeValue(x: T) { // NOT ALLOWED x.meow() } } 不应该是 out 并且该类具有获取参数类型的方法时的问题?很抱歉打扰,但对我来说,永远理解这个问题很重要
T 设为out 参数,则此假设不再有效,因为out 类型可以向上转换为超类型。
当您有一个泛型类G<T>(参数化类型)时,差异在于定义不同Ts 的类型G<T> 的层次结构与不同Ts 的层次结构之间的关系自己。
例如,如果子类 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> 实例中,我们实际上可以在声明了Child 类型参数的方法中接收到Parent 的实例,它不是Child 的子类。
【讨论】: