秒懂Kotlin之彻底弄懂形变注解out与in

[版权申明] 非商业目的注明出处可自由转载
博文地址: http://shusheng007.top/2020/09/23/2/
出自:shusheng007

概述

本文承接于上一篇:秒懂Kotlin之协变(Covariance)逆变(Contravariance)与抗变(Invariant),一定要先阅读这一篇文章,再阅读本文,不然看不懂!

上篇讲到Java中泛型是抗变的,但是数组却是协变的。Kotlin做的更彻底,不仅泛型是抗变的就连数组也变成抗变的了。

下面的代码是编译不过的

val strArray:Array<String> = arrayOf("shu","sheng","007")
val array:Array<Any> = strArray

报错:

Type mismatch: inferred type is Array<String> but Array<Any> was expected

泛型型变

官方文档见:Variance

Kotlin中没有通配符,取而代之的是 Declaration-site varianceUse-site variance 。其通过两个关键字outin来实现Java中的? extends? super 的功能.

假设我们有如下两个类和一个接口

open class Animal
class Dog : Animal()

interface Box<T> {
    fun getAnimal(): T
    fun putAnimal(a: T)
}

协变(out)

我们要定义一个方法,参数类型为Box<Animal>,但是我们希望可以传入Box<Dog>即希望可以发生协变

Java实现

private Animal getOutAnimalFromBox(Box<? extends Animal> box) {
   Animal animal = box.getAnimal();
//       box.putAnimal(? ) 没有办法调用修改方法,因为我们不知道?究竟是一个什么类型,没办法传入
   return animal;
}

Kotlin对应的实现为:

fun getAnimalFromBox(b: Box<out Animal>) : Animal {
    val animal: Animal = b.getAnimal()
//    b.putAnimal(Nothing) 无法调用,因为方法需要一个Nothing类型的对象,但是在kotlin中无法获取
    return animal
}

此方法可以接受Box<Dog>类型的参数了。

可见此处使用out 代替了? extends。从结果来看确实更合适一点,因为传入的参数只能提供值,而不能消费值。由于out是在方法调用的参数中标记的,处于使用端,所以叫Use-site varianceUse-site variance对应的就是Declaration-site variance了。

我们发现接口 Box<T>中既有消费值的方法fun putAnimal(a: T),又有提供值的方法fun getAnimal(): T,导致我们必须在使用侧告诉编译器我们要使用哪一类方法。那我们可以在声明接口的时候告诉编译器吗?答案是肯定的,但是就需要将接口拆分为只包含提供值的方法的接口producer与只包含消费值的方法的接口consumer

//producer
interface ReadableBox<out T> {
    fun getAnimal(): T
}
//consumer
interface WritableBox<in T> {
    fun putAnimal(a: T)
}

拆分完接口并做了相应的声明后,就可以不在使用端使用out或者in了。

fun getAnimalFromReadableBox(b: ReadableBox<Animal>){
    val a: Animal = b.getAnimal()
}

上面的方法可以直接接受ReadableBox<Dog>类型的参数,给人的感觉好像是Kotlin使得泛型协变了。

getAnimalFromReadableBox(object :ReadableBox<Dog>{
    override fun getAnimal(): Dog {
        return Dog()
    }
})

此种情况下outin是在声明时候使用的,所以叫Declaration-site variance了。

逆变(in)

我们要定义一个方法,参数类型为Box<Dog>,但是我们希望可以传入Box<Animal>,即希望可以发生逆变

Java实现

private void putAnimalInBox(BoxJ<? super Dog> box){
    box.putAnimal(new Dog());
    Object animal= box.getAnimal();// 可以调用读取方法,但是返回的类型确实Object,因为我们只能确定?的大基类是Object
}

Kotlin对应实现

fun putAnimalInBox(b: Box<in Dog>){
    b.putAnimal(Dog())
    val animal:Any? = b.getAnimal()// 可以调用读取方法,但是返回的类型确实Any?,因为我们只能确定?的大基类是Any?
}

此方法可以接受Box<Animal>类型的参数了

可见此处使用in 代替了? super,从结果来看确实更合适一点,因为传入的参数只适合消费值,而不适合获取值,获取到的值失去了有用的类型信息。由于in是在方法调用的参数中标记的,处于使用端,所以叫Use-site variance

让我们来看一下使用Declaration-site variance实现逆变

fun putAnimalToWritableBox(b:WritableBox<Dog>){
    b.putAnimal(Dog())
}

上面的方法可以直接接受WritableBox<Animal>类型的参数,给人的感觉好像是Kotlin使得泛型逆变了。

putAnimalToWritableBox(object :WritableBox<Animal>{
    override fun putAnimal(a: Animal) {
    }
})

总结

kotlin中令初学者费解的outin关键字就彻底将完了,相信你应该秒懂了。如果还是不懂,说明你太早看到这篇文章拉,建议你先收藏,以后等水平提高了再回来看看。

对了,记得点赞,分享。

暮云收尽溢清寒,银汉无声转玉盘。此生此夜不长好,明月明年何处看。《阳关曲 中秋月》苏轼

参考文章:

  1. Generics
  2. The Ins and Outs of Generic Variance in Kotlin

You May Also Like

About the Author: shusheng007

发表评论

邮箱地址不会被公开。