文章19 | 阅读 10445 | 点赞0
协变和逆变并不是Kotlin独有的概念,像Java、C#都有这样的概念,对于Java中协变和逆变的理解可参考笔者的另一篇文章。对于Java中协变和逆变的表示,可以通过这样一条PECS规则来概括:Producer Extends, Consumer Super.即仅仅是从该结构中读取数据,该结构就类似于一个生产者(Producer),此时使用"? extends Type";若仅仅是往该结构中放入数据,该结构就类似于一个消费者(Consumer),此时使用"? super Type"。此篇主要来介绍Kotlin中的协变(covariance)和逆变(contravariance)。
在此之前,我们先定义一些具有继承关系的类,旨在帮助我们解释清楚概念:
abstract class Fruit
open class Apple : Fruit()
class Pear : Fruit()
// 红富士苹果
class FujiApple : Apple()
我们先声明这样的一个泛型类:
class Producer<T>(private val t: T) {
fun produce(): T {
return t
}
}
main方法中:
fun main() {
// 苹果园
val appleProducer: Producer<Apple> = Producer(Apple())
// 梨园
val pearProducer: Producer<Pear> = Producer(Pear())
}
上面的代码没有任何毛病。但是如果我们现在想要表示一个综合性果园,既苹果又生产梨,那么用现有的结构就无法表示了。这个时候我们就需要修改Producer类:
class Producer<out T>(private val t: T) {
fun produce(): T {
return t
}
}
fun main() {
// 苹果园
val appleProducer: Producer<Apple> = Producer(Apple())
// 梨园
val pearProducer: Producer<Pear> = Producer(Pear())
// 综合性果园
val fruitProducer: Producer<Fruit> = Producer(Apple())
val fruitProducer2: Producer<Fruit> = Producer(Pear())
}
可以看过,我们在Producer类中添加了out关键字后,下面的两行代码也就OK了。
如果我们在Producer类中添加这样的一个consume方法:
class Producer<out T>(private val t: T) {
fun produce(): T {
return t
}
fun consume(t: T){
}
}
这个时候呢,编译就会报错:
从这里的提示我们可以这样去理解:所谓的’in’ position,就是方法的参数位置;所谓的’out’ position,就是方法的返回值位置。
那么也就是说,一旦用out关键字来使得类支持协变时,该泛型参数只能出现在方法的返回值上(构造器除外)。这实际上和前面提到的PECS是吻合的,只允许从Producer类中取出数据,而不允许向其中放入数据。
对比Java中协变的使用,Kotlin中这个方式叫做声明处协变(Declaration-site variance),Java中的方式叫做使用处协变(Use-site variance)。
总结一下:在Kotlin中使用out关键字来表示协变,并且是声明处协变;使用了协变之后,也就意味着该协变类型只能出现在方法的返回值处(构造器例外)。
理解了Kotlin中的协变之后,逆变的理解也就简单了,既然有out关键字,那自然有in关键字,Kotlin中用该关键字来表示逆变。
class Consumer<in T> {
fun consume(t: T){
}
}
val consumer: Consumer<Apple> = Consumer<Apple>()
val consumer2: Consumer<Apple> = Consumer<Fruit>()
相应地,如果添加了这样的produce()方法,编译器也会报类似的语法错误。那么逆变也可以总结一下:在Kotlin中使用in关键字来表示逆变,并且是声明处逆变;使用了逆变之后,也就意味着该逆变类型只能出现在方法的参数处(构造器例外)。
也叫做使用处变型(use-site variance)。为什么会有这种变型?我们一起来看一个类:Array。
public class Array<T> {
public operator fun get(index: Int): T
public operator fun set(index: Int, value: T): Unit
//.......
}
依据我们关于协变逆变的理论,泛型类型T既出现在out位置上,又出现在in位置,那么也就意味着我们既不能将Array声明为协变类型,也不能声明为逆变类型。我们再看这样一个copy方法:
fun copy(srcArr: Array<Any>, destArr: Array<Any>): Boolean {
if (srcArr.size > destArr.size) return false
for (index in srcArr.indices) {
destArr[index] = srcArr[index]
}
return true
}
这样就实现了数组的浅拷贝。现在我们想实现两个String数组的拷贝:
fun main() {
val srcArr: Array<String> = arrayOf("hello", "world", "welcome")
val destArr: Array<Any> = Array(3) { "" }
copy(srcArr, destArr)
}
此时会报类型不匹配,因为Array<T>中的T是不变的。此时对copy方法参数添加一个out关键字:
fun copy(srcArr: Array<out Any>, destArr: Array<Any>): Boolean {
if (srcArr.size > destArr.size) return false
for (index in srcArr.indices) {
destArr[index] = srcArr[index]
}
return true
}
此时就正常了。那么为什么呢?假定将Array<String>视为Array<Any>的子类型,那么会出现什么问题呢?
srcArr[0] = 1
这行代码在编译期间没有问题,但是在运行期就会出现异常。在Java里这种异常叫做java.lang.ArrayStoreException。
那么实际上换句话说,如果对srcArr没有set操作(没有出现在in position),那么这种运行期异常也就不复存在了,也就没有风险了。所以,我们在形参处,加入out关键字,使得发生使用处协变。这样:
如果还尝试如此去使用,编译器是不会允许你成功的。
同样地,对于使用处逆变:
fun setValue(array: Array<in String>, index: Int, value: String) {
array[index] = value
println(array[0])
}
只不过呢,它是允许出现在out位置,但是get出来的类型是Any?:
总结:对于类型投影,我们可以理解成,如果类声明是不变(意味着泛型类型既能出现在out位置,也能出现在in位置)的,那么我们可以在使用处将其声明成协变或者逆变,相当于把这个类型投影出某一面(in或out)进行使用。
语法形式:<>,例如MutableList<>,即将泛型类型声明成*
如何理解星投影呢?我们可以将MutableList<*>和MutableList<Any?>进行一个对比:
这两者从代码层面上还有个直观的区别:
你可以使用MutableList<Any?>()来创建一个对象,但是不能够使用MutableList<*>()来创建一个对象,后者只能出现在引用声明处。
那么星投影对于泛型类型支持协变、逆变以及不变三类分别有什么影响呢?在此之前,我们先来介绍一个类:Nothing。
val star: Producer<*> = Producer<Int>(1)
star.produce()
这样produce()方法的返回值类型是Any?
val star2: Consumer<*> = Consumer<Int>()
star2.consumer()
那么就不能consume任何数据了。
get()出来的类型是Any?,不能set()。
似乎星投影丢掉了很多信息,那么它的应用场景在哪呢?
fun printFirst(list: List<*>){
if (list.isNotEmpty()) {
println(list.first())
}
}
即你对类型参数并不care,像这里的printFirst()方法,我们仅仅想打印第一个元素,至于你到底是什么类型,是不关心的。
参考文献:《kotlin docs》、《kotlin in action》
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://blog.csdn.net/xlh1191860939/article/details/107482937
内容来源于网络,如有侵权,请联系作者删除!