ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
[TOC] ## 协变与逆变 类或者接口上的泛型参数可以添加out或者in关键字。 对于泛型类型参数, * **out关键字**用于指定该类型参数是**协变Covariant**; * **in关键字**用于指定该类型参数是**逆变Contravariance**。 协变与逆变其实是C#语言4.0以后新增的高级特性, * **协变是将父类变为具体子类,协变类型作为消费者,只能读取不能写入**, * **逆变是将子类变为具体父类,逆变作为生产者,只能写入不能读取**。 Kotlin语言在1.0版本时就将其引入到语法体系中来了。本节我们将对协变与逆变进行详细讲解。 ### 协变 在上一小节中的脚下留心中讲到,B类是A类的子类型,默认情况下Xxx<B>不是Xxx<A>的子类型。但是**通过out关键字可以使Xxx<B>是Xxx<A>的子类型,这样的操作叫作协变**。 接下来我们稍微修改上面的代码来通过out关键字可以让Xxx<B>是Xxx<A>的子类型。修改后的代码如下所示。 ``` open class Animal { fun bathe() { println("开开心心地洗澡...") } } class Cat : Animal() //猫类 class PetShop<T : Animal>(var animals: List<T>)//宠物店类 //帮所有的宠物洗澡 fun batheAll(petShop: PetShop<out Animal>) { for (animal: Animal in petShop.animals) { animal.bathe() //开始洗澡 } } fun main(args: Array<String>) { val cat1 = Cat() //第1只猫 val cat2 = Cat() //第2只猫 val animals = listOf<Cat>(cat1, cat2) //宠物装入一个集合 val petShop = PetShop<Cat>(animals) //将宠物送到宠物店 batheAll(petShop) // 编译器报错 } ``` 运行结果: ``` 开开心心地洗澡… 开开心心地洗澡… ``` 上述代码中,虽然Cat是Animal的子类型,但是`PetShop<Cat>`不是`PetShop<Animal>`的子类型,在第19行代码中调用batheAll()方法后程序正常运行,并不会像之前示例中的代码出现类型不匹配的编译问题,这是因为**在batheAll()方法的参数中添加了一个out关键字,这个关键字可以帮助泛型参数`<Animal>`支持协变(将父类变为具体子类)**。 >[info] **总结** out关键字使用的几种情况 (1)out关键字**只能出现在泛型类或者泛型接口的泛型参数声明上**,不能出现在泛型方法的泛型参数声明上。 (2)out关键字修饰泛型类或者泛型接口的泛型参数时会**支持协变**的 ### 逆变 根据上面章节中的脚下留心的内容可知,如果A是B的子类型,默认情况下Xxx<A>不是Xxx<B>的子类型,通过out关键字使Xxx<A>变成Xxx<B>的子类型时,这样的变化叫作协变。与out关键字对应的是in关键字,in关键字与out关键字有相反的功能,可以使Xxx<A>不是Xxx<B>的子类型,这样的变化叫作逆变。 接下来我们通过一个案例来验证in关键字使泛型参数支持逆变,具体代码如下所示。 ``` open class Animal class Cat : Animal() {} class Dog : Animal() {} //定义宠物店类 class PetShop<in T> { fun feed(animal: T) { if (animal is Cat) { println("喂食小猫...") } else if (animal is Dog) { println("喂食小狗...") } } } //定义喂猫的方法 fun feedCat(petShop: PetShop<Cat>): Unit { petShop.feed(Cat()) } fun main(args: Array<String>) { feedCat(PetShop<Animal>()) } ``` 运行结果: ``` 喂食小猫… ``` 上述代码中,feedCat()方法中需要传递的参数类型是`PetShop<Cat>`,在第19行调用这个方法时,传递的是`PetShop<Animal>`类型的参数,程序没报错并且运行成功。根据代码可知Cat是Animal的子类型,根据运行结果可知PetShop<Animal>是PetShop<Cat>子类型,这是由于在第5行代码中PetShop<in T>泛型参数上使用了in关键字,**in关键字使泛型参数产生了逆变(将子类变为具体父类)**。 >[info]总结 in关键字使用的几种情况 (1)in关键字**可以出现在泛型类或者泛型接口的泛型参数声明**上,**不能出现在泛型方法的泛型参数声明上**。 (2)in关键字修饰泛型类或者泛型接口中的泛型参数时会**支持逆变**。 (3)**泛型参数T在使用了in关键字之后,不能声明成val或者var类型的变量**。 ### 点变型(声明点变型和使用点变型) 前几个小节中说到的out、in关键字都是出现在类或者接口中的泛型参数声明的时候,这样做确实比较方便,因为它们的作用范围比较广,可以应用到所有类被使用的地方。这种**把out、in关键字放在泛型参数声明处的情况被称为声明点变型**。需要注意的是,**如果泛型参数中使用了var类型变量,则此处无法使用out、in关键字,也就不能声明点变型**。 **除了在类或接口中定义泛型参数时使用out、in关键字之外,还可以在泛型参数出现的具体位置使用out、in关键字**,这种变型被称为**使用点变型**。接下来我们通过一个案例来解释使用点变型。具体代码如下所示。 ``` open class Fruit(val name: String) open class Mammal(val name: String) class Banana : Fruit("香蕉") class Pear : Fruit("梨子") class Lion : Mammal("狮子") class Tiger : Mammal("老虎") class Forest<T>(var content: T) //打印Box中的Fruit的name fun printFruit(forest: Forest<out Fruit>) { println(forest.content.name) } //打印Box中的Animal的name fun printMammal(forest: Forest<out Mammal>) { println(forest.content.name) } fun main(args: Array<String>) { val bananaForest = Forest<Banana>(Banana()) val pearForest = Forest<Pear>(Pear()) val lionForest = Forest<Lion>(Lion()) val tigerForest = Forest<Tiger>(Tiger()) printFruit(bananaForest) printFruit(pearForest) printMammal(lionForest) printMammal(tigerForest) } ``` 运行结果: ``` 香蕉 梨子 狮子 老虎 ``` 上述代码中,在第7行代码中定义泛型Forest时,没有使用关键字out、in。由于在定义泛型Forest时,泛型参数中使用了var类型变量,因此也无法在此处使用out、in关键字,也就没有声明点变型。 在上述代码的**第8和12行中,在泛型参数出现的位置使用了out关键字,这个关键字使泛型参数进行了协变**。在printFruit()方法中传递的形参是`Forest<out Fruit>`类型,在第17和18行代码中向该方法中传递的是`Forest<Banana>`、`Forest<Pear>`类型,由于这两种类型都是`Forest<out Fruit>`的子类型,因此传递这两个类型都是可行的。同样,由于`Forest<Lion>`、`Forest<Tiger>`都是`Forest<out Mammal>`子类型,因此在printMammal()方法中传递这两个泛型类型的参数也是可以的。