写点什么

一篇文章教你搞清楚——Kotlin- 进阶 --- 不变型、协变

用户头像
Android架构
关注
发布于: 14 小时前

协变的意思是:类与其类型参数的抽象程度具有相同的变化方向。


(试图总结某个抽象概念时,总会说出一些让人听不懂话。。。)


换句话说:当类型参数变得更具体时,类也变得更具体。当类型参数变得更抽象时,类也变得更抽象。


比如,从 List<String>到 List<CharSequence>,类型参数从 String 变为更抽象的 CharSequence,如果 List<String>到 List<CharSequence>的变化方向也是更抽象(前者是后者的子类),就称 List<T>在类型参数 T 上是协变的。(显然这个例子不是协变的而是不变型的)


如果一个泛型类是协变的,就意味着它在类的层面保留了类型参数的子类型关系


Kotlin 中,声明类在类型参数上是协变的,需要添加 out 保留字:


class MyList<out T>{ ... }


虽然将泛型类声明为协变可以让其子类型化关系更符合直觉,但这需要付出代价:


class MyList<out T> {fun set(item: T) {}//报错: Type parameter is declare as "out" but occur at "in" position in type Tfun get(): T {...}}


  • 若 T 出现在方法的参数位,称 set(item: T)消费类型为 T 的值。

  • 若 T 出现在返回值位时,称 get(): T 生产类型为 T 的值。


当 T 被 out 修饰后,它只能出现在返回值位,即它只能被泛型类生产而不能被消费。所以 out 会产生两个效果:


1.它保留了泛型类的子类型化。2.它限制了类型参数只能出现在返回值位。


这两点是相辅相成的:正因为它限制了类型参数不能出现在参数位,所以子类型化得以保留。正因为它保留了子类型化,所以类型参数只能出现在返回值位。


假设类型参数出现在了参数位,就会出现在这样的情况:


class MyList<String> {fun set(itme: String)fun get(): String}


class MyList<CharSequence> {fun set(itme: CharSequence)fun get(): CharSequence}复制代码因为 fun set(itme: String)可以接收的参数范围比 fun set(itme: CharSequence)小,不符合第一条退推论,所以 MyList<String>不是 MyList<CharSequence>的子类型。而添加了 out 之后,相当于告诉编译器把出错的方法删掉以保留子类型化:class MyList<out T> {fun get(): T {...}}


class MyList<String> {fun get(): String}


class MyList<CharSequence> {fun get(): CharSequence}


此时 fun get(): String 返回值的范围比 fun get(): CharSequence 小,符合第二条推论,所以 MyList<String>是 MyList<CharSequence>的子类型。

逆变



除了不变型、协变,泛型类之间还有一种子类型关系:逆变。


逆变的意思是:类与其类型参数的抽象程度具有相反的变化方向。


换句话说:当类型参数变得更具体时,类却变得更抽象。当类型参数变得更抽象时,类却变得更具体。逆变有一点反直觉,它想实现的效果是:List<CharSequence>成为 List<String>是的子类型。


如果一个泛型类是逆变的,就意味着它在类的层面反转了类型参数的子类型关系


Kotlin 中,声明类在类型参数上是逆变的,需要添加 in 保留字:


class MyList<in T>{ ... }


同样地,这需要付出代价:


class MyList<in T> {fun set(item: T) {}fun get(): T {...}//报错: Type parameter is declare as "in" but occur at "out" position in type T}


当 T 被 in 修饰后,它只能出现在参数位,即它只能被泛型类消费而不能被生产。由此可见:


out 和 int 不仅限定了参数可以出现的位置,还限定了什么类可以成为子类型。

类型投影



生活中的投影,是把一个三维物体变成二维物体,投影看上去还是那个物体只是降了一维。程序中的类型投影也是类似的意思:


将类型投影意味着保留该类型的有些能力,去掉另一些能力。通过类型投影可以动态地改变泛型类的子类型关系。


类型投影通常应用于将不变型的泛型类动态地转换成逆变或协变。比如,MutableList 就是不变型的:


public interface MutableList<E> : List<E>, MutableCollection<E> {// 类型参数出现在 out 位置 public fun removeAt(index: Int): E// 类型参数出现在 in 位置 public fun add(index: Int, element: E): Unit...}


MutableList<E>是不变型,所以泛型参数可随意出现在 in 或 out 位置。


但不变型有时候会缩小方法的适用范围,比如:


fun <T> copy(source: MutableList<T>, destination: MutableList<T>){for (item in source){destination.add(item)}}


这是一个拷贝集合的方法,引入泛型是为了避免为每一种具体的类型都重新定义一遍方法。现在这个方法可以在任何数据类型相同的两个列表见拷贝内容。


但如果我想把一个字符串集合拷贝到可以包含任意对象的集合中怎么办?


val strings = mutableListOf( "a", "b", "c" )val anys = mutableListOf<Any>()


copy( strings, anys )// 报错


因为 copy()的定义要求源和目的集合具有相同的类型。


为了让 copy()方法能适用于这种情况,可以这样改写:


fun <R: T, T> copy(source: MutableList<R>, destination: MutableList<T>){for (item in source){destination.add(item)}}


引入第二个泛型 R,它是 T 的子类型,并指定它为源集合类型参数。


这个改动一下子扩展了 source 参数接受实参类型的范围,原本它只能和 destination 使用同样的类型,现在它可以使用所有 destination 的子类型。


运用变型可以简化这个改动:


fun <T> copy(source: MutableList<out T>, destination: MutableList<T>){for (item in source){destination.add(item)}}


source 参数声明的地方,发生了 out 类型投影,投影去掉了 MutableList 中所有消费类型参数的方法,保留了所有生产类型参数的方法。


虽然 source 参数丧失了部分能力,但牺牲总是有回报的,source 所能接受实参的类型范围被扩展了。(碰巧,copy()方法体中也不需要 source 丧失的那部分能力。)


MutableList<out T>和 MutableList<T>的区别如下:


public interface MutableList<out T> {// 类型参数出现在 out 位置的方法保持原样 public fun removeAt(index: Int): T// 类型参数出现在 in 位置的方法被改写 public fun add(index: Int, element: Nothing): Unit...}


out 保留字命令编译器去改写泛型类中所有消费类型参数的方法,将 in 位置的参数类型改成 Nothing。Nothing 是所有类的子类,为啥要这样改呢?因为想让 MutableList<String>成为 MutableList<out CharSequence>的子类型。回忆一下子类型的 2 个推论:


1.子类型方法接收的参数范围 不得小于 父类型方法 2.子类型方法返回的结果的范围 不得大于 父类型方法


当没有 out 投影时,public fun add(index: Int, element: String): Unit 接收参数的范围 小于 public fun add(index: Int, element: CharSequence): Unit 接收参数的范围,它不符合第一条规则,所以 MutableList<String>不是 Group<out CharSequence>的子类型。


换成 public fun add(index: Int, element: Nothing): Unit 后,情况就大不一样了。Nothing 是所有类的子类,它也不能被实例化,并且没有子类型。换句话说如果一个方法接收 Nothing 类型的参数,意味着没有任何类型可以作为参数传入(唯一可以传入的 Nothing 却不能实例化)。这样的话 public fun add(index: Int, element: String): Unit 接收参数的范围就比“什么也不能接收”大了(好歹它能接收 String 类型)。


类似地,in 保留字命令编译器去改写泛型类中所有生产类型参数的方法,将 out 位置的参数改成 Any?。


public interface MutableList<in T> {// 类型参数出现在 out 位置的方法被改写 public fun removeAt(index: Int): Any?// 类型参数出现在 in 位置的方法被保留 public fun add(index: Int, element: T): Unit...}


Any?是所有类的父类,这也就很巧妙地让 MutableList<in T>符合了第二条推论(Any?已经是最大的范围了,随便返回什么类型都是它的子类)


最后,还有一种特殊的投影叫 star 投影,它的效果是 in 投影和 out 投影之和。(详见下表)


Kotlin 中一共有三种类型投影,总结如下(其中,Group、Dog、Animal 都是类名,且 Dog 是 Animal 的子类型):![](https://upload-images.jianshu.io/upload_images/24216715-7f5c390be23a54e1.png?imageMogr2/auto-orient/strip%7


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


CimageView2/2/w/1240)

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
一篇文章教你搞清楚——Kotlin-进阶---不变型、协变