Kotlin协变和逆变概念及解析

协变和逆变

概念:

        Kotlin 的协变与逆变统称为 Kotlin 的变型。变型是指泛型的基础类型与它的参数类型是如何关联的。对于普通类型来说,我们可以使用子类代替父类,因为子类包含了父类的全部内容。但是对于泛型来说,如果泛型的基础类型相同,其中一个参数类型是另外一个参数类型的子类,泛型类也不存在这种继承关系,无法直接替换使用。要解除这些限制,就需要用到协变与逆变。

先声明两个泛型接口

//未加 out
interface Production<T>{
    fun product():T
}
//未加 in
interface Consumer<T>{
    fun consume(item:T)
}

在声明三个类,继承关系:Burger—extend—>FastFood—extend—>Food

open class Food {}
open class FastFood :Food(){}
class Burger:FastFood(){}

接着声明食品工厂

class FoodShop : Production<Food>{
    override fun product(): Food {
        return Food()
    }
}
class FastFoodShop : Production<FastFood>{
    override fun product(): FastFood {
        return FastFood()
    }
}

协变( OUT )

main函数创建这两个食品工厂   

fun main() {
//协变
    var foodshop:Production<Food> = FoodShop()//正常编译
    var fastfoodshop:Production<Food> = FastFoodShop() //报错
}

此时第4行代码相当于 Production<Food>类型引用指向Production<FastFood>类型对象。

        由于Production<Food>并不是Production<FastFood>的父类,相互之间无继承关系,所以编译器并不能匹配正确的引用类型。

        Java为了解决这个问题提供了<? Extends T>通配符,而在Kotlin中将这个通配符用out关键字来替代。需要在Production接口泛型T前加out关键字,表明泛型可以传入参数类及其子类

//加 out 编辑通过
interface Production<out T>{
    fun product():T
}

不是所有类都可以变成协变的

概念:

        只有当这个类只能生产类型 T 的值而不能消费它们时,才能变成协变的。就是说 T 的值只能作为函数返回值时,才能变成协变的。这也是为什么协变关键词叫做 out,表明它只能作为生产者对外输出。

        以下代码中,t : T 接收入参的位置叫做 in 位置,表示它是函数参数,是消费者。: T 返回值输出数据 的位置叫做 out 位置,表示它是函数返回值,是生产者。

Kotlin
interface Transformer<T> {
    //绿色为in位置,橙色为out位置
    fun transform(t: T): T
}

举个例子:

//Apple和Orange都是Fruits的子类
open class Fruits(val name: String) {}
class Apple(val n: String, val male: String = "apple"): Fruits(n) {}
class Orange(val n: String, val female: String = "orange"): Fruits(n) {}
//先声明个泛型类,内部封装了一个私有的data属性并向外部提供set、get方法
class MyClass<T> {
    private var data: T? = null
    fun set(t: T?) {
        data = t
    }
    fun get(): T? = data
}
fun test(data:MyClass<Fruits>) {
    data.set(Orange("橙子"))
}

定义测试主函数

        可以看到test传入参数爆红,这就和最开始说的Production<Food>并不是Production<FastFood>的父类是同样的问题,如果说kotlin允许这样跨继承传参(即test方法调用时不编译报错),那data.get()拿到的就是一个Orange类型的对象而data.get()需要返回的是一个Apple类型的对象,这样就会发生类型转换异常。

        所以kotlin是不允许这样去跨继承传参的,而换个角度想之所以这样写会出现类转换异常就是因为test方法中给set了一个Orange对象导致了问题,如果说MyClass在泛型T上是只读的即没有set方法那么就不会因为set一个Orange对象导致类型转换异常。

所以kotlin规定如果个泛型类或泛型接口定义out协变后,泛型T只能出现在out位置不能出现在in位置,即只能读不能写

逆变 ( IN )

逆变和协变是相反的,但其实道理是一样的

之所以第一句编译报错就是因为 ArrayList<Apple>并不是 ArrayList<Fruits>的子类无法进行类型强转。同样的,java为了解决这个问题提供了<? super T>通配符,而在kotlin中将这个通配符用in关键字来替代,in修饰Apple表明pl这个集合对象中存的可以是Apple及其父类的对象:

同样的定义个泛型接口

interface MyClass<T> {
    fun show(d: T?): T?
}
fun testTwo(data:MyClassTwo<Apple>){
    var result = data.show(Apple("apple"))
}

定义测试主函数

        可以看到调用时编译报错了,还是因为kotlin不支持直接跨继承传参。如果说编译不报错,那么继续走下去会看到形参dataApple类型的MyClassTwo引用,result变量要求接收一个Apple类型实现的MyClassTwo对象,但是实际上实参datashow方法返回了一个Orange对象,由于它是Fruits的子类,所以data在实现show方法的时候并没有问题,但是result在接收的时候无法将Apple强转为Orange类型,这样就会发生类型转换异常

        和协变一样,我们换个角度想之所以这样写会出现类转换异常就是因为show方法要去返回一个Fruits对象导致了问题,如果说MyClassTwo在泛型T上是只写的即不允许泛型T出现在out位置上,那么就不会因为show方法返回了一个Orange对象而导致的类型转换问题。

kotlin为了实现这个只能写不能读的功能而提供了in关键字,即这个泛型T只能出现在in位置不能出现在out位置

总结

        总的来说协变和逆变是java为了处理泛型的类型擦除而带入的新规则,kotlin在java的基础上用了out和in两个关键字来实现

out和in使用规则:

猜你喜欢

转载自blog.csdn.net/yb941693493/article/details/127788399