Kotlin 进阶 | 不变型、协变、逆变子类型泛型中的子类型不变型协变逆变PECS 原则 & POCI 原则类型投影推荐阅读

引入泛型之后,子类型的概念变得复杂,好不容易用刚学会的泛型定义了方法,用起来编译器却各种障碍。且听我把概念敲碎了再拼起来,娓娓道来。

 

子类型


任何时候,如果要使用 类型A 的值,都能用 类型B 的值作为替换(当做 A 的值),称 B 是 A 的子类型

 

从定义中可以看出,任何类型也是它自身的子类型。

 

把定义说的通俗一点就是 “小范围的类可以替换大范围的类”IntNumber的子类,是因为Int所代表的数的集合范围是Number所代表的子集。

 

“一个类是否是另一个的子类型”?这个问题对于编译器来说很重要,因为每次给变量赋值或为函数传递实参时都要做这个检查。只有值的类型是变量的子类型时,才允许变量存储该值。

 

泛型中的子类型


引入泛型之前,子类型的定义很明确,想要判断一个类是否是另一个的子类型也颇为直观,比如StringCharSequence的子类。

 

一旦引入泛型后,就变得复杂,比如:ListList的子类吗?换句话说,所有使用List的地方都能用List代替吗?其实还蛮难一下子做出判断的。

 

为了解决这个难题,需要在原始定义的基础上推导出两个推论:

 

  1. 子类型方法接收参数的范围 不得小于 父类型方法

 

  1. 子类型方法返回值的范围 不得大于 父类型方法 只有满足了这两个条件,才能无副作用地将程序中父类对象都替换成子类对象(无副作用即是程序符合原有的逻辑)。

 

如果List要成为List的子类就必须满足下面两个条件:

 

  1. List中方法接收参数的范围 不得小于 List的方法

 

  1. List中方法返回值的范围 不得大于 List的方法

 

List定义如下:

 

// List带泛型的定义
public interface List extends Collection {
    boolean add(E e);
    E get(int index);
}

// List定义如下:
public interface List extends Collection {
    boolean add(String e);
    String get(int index);
}

// List定义如下:
public interface List extends Collection {
    boolean add(CharSequence e);
    CharSequence get(int index);
}

 

虽然String get(int index);返回值的范围比CharSequence get(int index);小,满足了第二个条件。

 

boolean add(String e);接收参数的范围明显比boolean add(CharSequence e);要小,不满足第一个条件。

 

所以List不是List的子类型。换句话说,把程序中的List都替换成List是不安全的,因为可能会存在这样的代码:

 

val spannable = Spannable()
val list: List = mutableListOf()
list.add(spannable)

 

如果把这里的List换成List,编译器就会报错。因为boolean add(String e);只会处理String类型的实参,Spannable超出了这个范围。

 

不变型


把上面的例子表达成更抽象的定义如下:

 

假设 泛型 类A 包含 类型参数T,即class A,而Type1Type2的子类,如果AA不存在父子关系,则称 类A 在类型参数上是不变型的

 

Kotlin 和 Java 中的类都是不变型的。

 

这样会造成一些限制,辛辛苦苦抽象了一个方法,它接收一个List作为参数:handle(chars: List),想当然地把List传递进入时,编译器会报错。。。难道需要为List重新定义一个相同的方法吗?

 

协变


不变型描述的是泛型类之间没有子类型关系,泛型类之间还有一种子类型关系叫协变

 

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

 

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

 

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

 

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

 

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

 

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

 

class MyList{ ... }

 

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

 

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

 

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

 

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

 

Tout修饰后,它只能出现在返回值位,即它只能被泛型类生产而不能被消费。

 

所以out会产生两个效果:

 

  1. 它保留了泛型类的子类型化。

 

  1. 它限制了类型参数只能出现在返回值位。

 

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

 

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

 

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

class MyList {
    fun set(itme: CharSequence)
    fun get(): CharSequence
}

 

因为fun set(itme: String)可以接收的参数范围比fun set(itme: CharSequence)小,不符合第一条退推论,所以MyList不是MyList的子类型。

 

而添加了out之后,相当于告诉编译器把出错的方法删掉以保留子类型化:

 

class MyList {
    fun get(): T {...}
}

class MyList {
    fun get(): String
}

class MyList {
    fun get(): CharSequence
}

 

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

 

逆变


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

 

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

 

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

 

逆变有一点反直觉,它想实现的效果是:List成为List是的子类型。

 

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

 

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

 

class MyList{ ... }

 

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

 

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

 

Tin修饰后,它只能出现在参数位,即它只能被泛型类消费而不能被生产。

 

由此可见:

 

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

 

PECS 原则 & POCI 原则


PECS = producer extends,consumer super,即如果泛型类生产泛型对象,则使用 extends T>通配符表示协变。如果泛型类消费泛型对象,则使用 super T>通配符表示逆变。

 

Kotlin 中使用更简单的out, in表达协变和逆变。所以 PECS 原则在 Kotlin 中可以表述为POCI 原则

 

类型投影


生活中的投影,是把一个三维物体变成二维物体,投影看上去还是那个物体只是降了一维。

 

程序中的类型投影也是类似的意思:

 

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

 

类型投影通常应用于将不变型的泛型类动态地转换成逆变协变

 

比如,MutableList就是不变型的:

 

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

 

MutableList是不变型,所以泛型参数可随意出现在inout位置。

 

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

 

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

 

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

 

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

 

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

copy( strings, anys )// 报错

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

MutableListMutableList的区别如下:

 

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

 

out保留字命令编译器去改写泛型类中所有消费类型参数的方法,将in位置的参数类型改成Nothing

 

Nothing是所有类的子类,为啥要这样改呢?因为想让MutableList成为MutableList的子类型

 

回忆一下子类型的 2 个推论:

 

  1. 子类型方法接收的参数范围 不得小于 父类型方法

 

  1. 子类型方法返回的结果的范围 不得大于 父类型方法

 

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

 

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

 

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

 

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

 

Kotlin 中一共有三种类型投影,总结如下(其中,Group、Dog、Animal都是类名,且 Dog 是 Animal 的子类型):

 

投影类型 投影实例 变型 继承关系 限制
out 投影 Group< out Animal > 协变 Group< Dog > 是 Group< out Animal > 子类 类型参数不能作为方法参数
in 投影 Group< in Animal > 逆变 Group< in Animal > 是 Group< Dog > 子类 类型参数不能作为方法返回值
star 投影 Group< * > Group< 任何类型 > 都是 Group< * > 子类 类型参数不能做方法参数也不能做返回值

 

推荐阅读


 

 

 

 

 

 

 

 

 

文章来源于互联网:Kotlin 进阶 | 不变型、协变、逆变子类型泛型中的子类型不变型协变逆变PECS 原则 & POCI 原则类型投影推荐阅读

下载说明:
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.dandroid.cn/19858,转载请注明出处。
0

评论0

显示验证码
没有账号?注册  忘记密码?