简述: 前几天咱们一块儿为Kotlin中的泛型型变作了一个很好的铺垫,深刻分析下类型和类,子类型和子类之间的关系、什么是子类型化关系以及型变存在的意义。那么今天将会讲点更刺激的东西,也就是Kotlin泛型型变中最为难理解的地方,那就是Kotlin中的协变、逆变、不变。虽然很难理解,可是有了上篇文章基础教你如何攻克Kotlin中泛型型变的难点(上篇)理解起来仍是相对比较轻松。若是你是初学者不建议直接看这篇文章,仍是建议把该系列的上篇理解下。安全
扯会皮,这几天我一直在思考一个问题,由于官方给出的结论太过于正式化,并且估计好点的开发者只是记住官方的结论和它的使用规则,可是并无真正去了解为何是这样的,这样设计的意义何在呢?app
废话很少说,继续上本篇文章的思惟导图less
还记得上篇的子类型化关系吗?协变实际上就是保留子类型化关系,首先,咱们须要去明确一下这里所说的保留子类型化关系是针对谁而言的呢?ide
来看个例子,String
是String?
的子类型,咱们知道基础类型List<out E>
是协变的,那么List<String>
也就是List<String?>
的子类型的。很明显这里针对的角色就是List<String>
和List<String?>
,是它们保留了String
到String?
的子类型化关系。或者换句话说两个具备相同的基础类型的泛型协变类型,若是类型实参具备子类型化关系,那么这个泛型类型具备一致方向的子类型化关系。那么具备子类型化关系实际上子类型的值能在任什么时候候任何地方替代超类型的值。函数
interface Producer<out T> {//在泛型类型形参前面指定out修饰符
val something: T
fun produce(): T
}
复制代码
从上面定义的基本结构来看,实际上协变点就是上面produce
函数返回值的T
的位置,Kotlin中规定一个泛型协变类,在泛型形参前面加上out修饰后,那么修饰这个泛型形参在函数内部使用范围将受到限制只能做为函数的返回值或者修饰只读权限的属性。源码分析
interface Producer<out T> {//在泛型类型形参前面指定out修饰符
val something: T//T做为只读属性的类型,这里T的位置也是out协变点
fun produce(): T//T做为函数的返回值输出给外部,这里T的位置就是out协变点
}
复制代码
以上协变点都是标准的T类型,实际上如下这种方式其实也是协变点,请注意体会协变点含义:post
interface Producer<out T> {
val something: List<T>//即便T不是单个的类型,可是它做为一个泛型类型修饰只读属性,因此它所处位置仍是out协变点
fun produce(): List<Map<String,T>>//即便T不是单个的类型,可是它做为泛型类型的类型实参修饰返回值,因此它所处位置仍是out协变点
}
复制代码
协变点基本特征: 若是一个泛型类声明成协变的,用out修饰的那个类型形参,在函数内部出现的位置只能在只读属性的类型或者函数的返回值类型。相对于外部而言协变是生产泛型参数的角色,生产者向外输出out学习
List<out E>
的源码分析咱们在上篇文章中就说过Kotlin中的List
并非Java中的List
,由于Kotlin中的List
是个只读的List不具有修改集合中元素的操做方法。Java的List
实际上至关于Kotlin中的MutableList
具备各类读和写的操做方法。ui
Kotlin中的List<out E>
实际上就是协变的例子,用它来讲明分析协变最好不过了,还记得上篇文章说过的学习泛型步骤二吗,就是经过分析源码来验证本身的理解和结论。经过如下源码都可验证咱们上述所说的结论。this
//经过泛型类定义能够看出使用out修饰符 修饰泛型类型形参E
public interface List<out E> : Collection<E> {
override val size: Int
override fun isEmpty(): Boolean
override fun contains(element: @UnsafeVariance E): Boolean//咦! 咦! 咦! 和说的不同啊,为何还能出如今这个位置,还出来了个@UnsafeVariance 这个是什么鬼? 告诉你,稳住,先不要急,请听我在后面慢慢说来,先暂时保留神秘感
override fun iterator(): Iterator<E>//这里明显能看出来E处于out协变点位置,并且仍是泛型类型Iterator<E>出现的,正好验证咱们上述所说的协变的变种类型(E为类型实参的泛型类型)
override fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean
public operator fun get(index: Int): E//函数返回值的类型E,这里明显能看出来E处于out协变点位置,正好验证咱们上述所说的协变的标准类型(E直接为返回值的类型)
public fun indexOf(element: @UnsafeVariance E): Int
public fun lastIndexOf(element: @UnsafeVariance E): Int
public fun listIterator(): ListIterator<E>//(E为类型实参的泛型类型),为out协变点
public fun listIterator(index: Int): ListIterator<E>//(E为类型实参的泛型类型),为out协变点
public fun subList(fromIndex: Int, toIndex: Int): List<E>//(E为类型实参的泛型类型),为out协变点
}
复制代码
源码分析完了,是否是感受仍是有点迷惑啊?就是E为啥还能在其余的位置上,还有@UnsafeVariance是个什么鬼? 这些疑问先放一放,可是上述至少证实了泛型协变out协变的位置是返回值的类型以及只读属性的类型(这点源码中没有表现出来,可是实际上倒是如此啊,这里能够自行查阅其余例子)
逆变实际上就是和协变子类型化关系正好相反,它是反转子类型化关系。
来个例子说明下,咱们知道String
是String?
的子类型,Comparable<in T>
是逆变的,那么Comparable<String>
到Comparable<String?>
其实是反转了String
到String?
的子类型化关系,也就是和String
到String?
的子类型化关系相反,那么Comparable<String?>
就是Comparable<String>
子类型, Comparable<String>
类型值出现的地方均可用Comparable<String?>
类型值来替代。
换句话说就是:两个具备相同的基础类型的泛型逆变类型,若是类型实参具备子类型化关系,那么这个泛型类型具备相反方向的子类型化关系
interface Consumer<in T>{//在泛型类型形参前面指定in修饰符
fun consume(value: T)
}
复制代码
从上面定义的基本结构来看,实际上逆变点就是上面consume
函数接收函数形参的T
的位置,Kotlin中规定一个泛型协变类,在泛型形参前面加上out修饰后,那么修饰这个泛型形参在函数内部使用范围将受到限制只能做为函数的返回值或者修饰只读权限的属性。
interface Consumer<in T>{//在泛型类型形参前面指定in修饰符
var something: T //T做为可变属性的类型,这里T的位置也是in逆变点
fun consume(value: T)//T做为函数形参类型,这里T的位置也就是in逆变点
}
复制代码
和协变相似,逆变也存在那种泛型类型处于逆变点的位置,这些咱们均可以把当作逆变点:
interface Consumer<in T>{
var something: B<T>//这里虽然是泛型类型可是T所在位置依然是修饰可变属性类型,因此仍处于逆变点
fun consume(value: A<T>)//这里虽然是泛型类型可是T所在位置依然是函数形参类型,因此仍处于逆变点
}
复制代码
逆变点基本特征: 若是一个泛型类声明成逆变的,用in修饰泛型类的类型形参,在函数内部出现的位置只能是做为可变属性的类型或者函数的形参类型。相对于外部而言逆变是消费泛型参数的角色,消费者请求外部输入in
Comparable<in T>
的源码分析在Kotlin中其实最简单的泛型逆变的例子就是Comparable<in T>
public interface Comparable<in T> {//泛型逆变使用in关键字修饰
/** * Compares this object with the specified object for order. Returns zero if this object is equal * to the specified [other] object, a negative number if it's less than [other], or a positive number * if it's greater than [other]. */
public operator fun compareTo(other: T): Int//由于是逆变的,因此T在函数内部出现的位置做为compareTo函数的形参类型,能够看出它是属于消费泛型参数的
}
复制代码
对于不变就更简单了,泛型型变中除去协变、逆变就是不变了。其实不变看起来就是咱们经常使用的普通泛型,它既没有in关键字修饰,也没有out关键字修饰。它就是普通的泛型,因此很明显它没有像协变、逆变那样那么多的条条框框,它很自由既可读又可写,既能够做为函数的返回值类型也能够做为函数形参类型,既能够声明成只读属性的类型又能够声明可变属性。可是注意了:不变型就是没有子类型化关系,因此它会有一个局限性就是若是以它做为函数形参类型,外部传入只能是和它相同的类型,由于它根本就不存在子类型化关系说法,那也就是没有任何类型值可以替换它,除了它本身自己的类型 例如MutableList<String>和MutableList<String?>
是彻底两种不同的类型,尽管String
是String?
子类型,可是基础泛型MutableList<E>
是不变型的,因此MutableList<String>和MutableList<String?>
根本不要紧。
interface MutableList<E>{//没有in和out修饰
fun add(element: E)//E能够做为函数形参类型处于逆变点,输入消费E
fun subList(fromIndex: Int, toIndex: Int): MutableList<E>//E又能够做为函数返回值类型处于协变点,生产输出E
}
复制代码
思考一:
协变泛型类的泛型形参类型T必定就只能out协变点位置吗?能不能在in逆变点位置呢?
解惑一: 能够在逆变点,可是必须在函数内部保证该泛型参数T不存在写操做行为,只能有读操做
出现的场景: 声明了协变的泛型类,可是有时候须要从外部传入一个该类型形参的函数参数,那么这个形参类型就处于in逆变点的位置了,可是函数内部可以保证不会对泛型参数存在写操做的行为。常见例子就是List<out E>
源码,就是上面你们一脸懵逼的地方,就是那个为何定义成协变的泛型T跑到了函数形参类型上去。 以下面部分代码所示:
override fun contains(element: @UnsafeVariance E): Boolean//咦! 咦! 咦! 和说的不同啊,为何还能出如今这个位置,还出来了个@UnsafeVariance 这个是什么鬼? 如今回答你就是可能会出如今这,可是只要保证函数不会写操做便可
复制代码
上述的List中的contains函数形参就是泛型形参E,它是协变的出如今逆变点,可是只要保证函数内部不会对它有写操做便可
思考二:
逆变泛型类的泛型形参类型T就必定只能在in逆变点位置吗?能不能在out协变点位置呢?
解惑二: 同理,也能够在协变点位置
思考三:
能在其余的位置吗? 好比构造函数
解惑三: 能够在构造器函数中,由于这是个比较特殊的位置,既不在in位置也不在out位置
class ClassMates<out T: Student>(vararg students: T){//能够看到虽然定义成了协变,可是这里的T不是在out协变点的位置,这种声明依然是合法的
...
}
复制代码
注意: 这里就是很特殊的场景了,因此开头就说过了若是把这些规则,用法只是死记硬背下来,碰到这种场景的时候就开始怀疑人生了,规则中不是这样的啊,规则中定义协变点就是只读属性类型和函数返回值类型的位置啊,这个位置不上不下的该怎么解释呢?因此解决问题仍是须要抓住问题的关键才是最主要的。
其实解释这个问题也不难,回到型变的目的和初衷上去,型变是为了解决类型安全问题,是防止更加泛化的实例调用某些存在危险操做的方法。构造函数很特殊通常建立后实例对象后,在该对象基础上构造函数是不能再被调用的,因此这里T放在这里是安全的。
思考四
为了安全,我是否是只要把全部泛型类全都定义成协变或逆变或不变一种就能够了呢?
解惑四: 不行,这样不安全,按照实际场景需求出发,一味定义成协变或逆变实际上限制了该泛型类对该类型形参使用的可能性,由于out只能是做为生产者,协变点位置有限制,而in只能是消费者逆变点的位置也有限制。那索性全都定义成不变型,那就在另外一层面丧失了灵活性,就是它失去了子类型化关系, 就是把它做为函数参数类型,外部只能传入和它相同的类型,不可能存在子类型化关系的保留和反转了
由上面的思考明白了一点,使用协变、逆变的时候并非那么死的按照协变点,逆变点规则来,能够更加灵活点,关键是不能违背协变、逆变根本宗旨。协变宗旨就是定义的泛型类内部不能存在写操做的行为,对于逆变根本宗旨通常都是只写的。那Kotlin中List<out E>
的源码来讲都不是真正规则上说的那样协变,泛型形参E并不都是在协变点out上,可是List<out E>
内部可以保证不会存在写操做危险行为因此这种定义也是合法。实际上真正开发过程,很难作到协变泛型类中的泛型类型形参都是在out协变点上,由于有时候需求须要确实须要从外部传入一个该类型形参的一个函数形参。
因此最终的结论是: 协变点out和逆变点in的位置的规则是通常大致状况下要遵照的,可是须要具体状况具体分析,针对设计的泛型类具体状况,适当地在不违背根本宗旨以及知足需求状况下变下协变点和逆变点的位置规则
由上面的本质区别分析,严格按照协变点、逆变点规则来是不能彻底知足咱们真实开发需求场景的,因此有时候须要一道后门,那就要用特殊方式告诉它。那就是使用UnSafeVariance注解。因此UnSafeVariance注解做用很简单: 经过@UnSafeVariance告诉编译器该处安全性本身可以把控,让它放你编译经过便可,若是不加编译器认为这是不合法的。注解的意思就是不安全的型变,例如在协变泛型类中有个函数是以传入一个该泛型形参的函数形参的,经过UnSafeVariance注解让编译器闭嘴,而后把它放置在逆变点其实是增长一层危险性,至关于把这层危险交给了开发者,只要开发者能保证内部不存在危险性操做确定就是安全的。
将从基本结构形式、有无子类型化关系(保留、反转)、有无型变点(协变点out、逆变点in)、角色(生产者输出、消费者输入)、类型形参存在的位置(协变就是修饰只读属性和函数返回值类型;逆变就是修饰可变属性和函数形参类型)、表现特征(只读、可写、可读可写)等方面进行对比
协变 | 逆变 | 不变 | |
---|---|---|---|
基本结构 | Producer<out E> |
Consumer<in T> |
MutableList<T> |
子类型化关系 | 保留子类型化关系 | 反转子类型化关系 | 无子类型化关系 |
有无型变点 | 协变点out | 逆变点in | 无型变点 |
类型形参存在的位置 | 修饰只读属性类型和函数返回值类型 | 修饰可变属性类型和函数形参类型 | 均可以,没有约束 |
角色 | 生产者输出为泛型形参类型 | 消费者输入为泛型形参类型 | 既是生产者也是消费者 |
表现特征 | 内部操做只读 | 内部操做只写 | 内部操做可读可写 |
实际上就是要明确何时该使用协变、何时该使用逆变、何时该使用不变。 实际上经过上述分析对比的表格能够得出结论: 首先,表格有不少个条件特征,究竟是先哪一个开始断定条件好呢?实际上这里面仍是须要选择一下的。
假设一: 就好比一开始就以有无使用子类型化关系为条件作断定,这样作法是有点问题的,试想下在实际开发中,先是去定义泛型类内部一些方法和属性的,这时候很难知道在外部使用状况下存不存在利用子类型化关系,也就是存不存在用子类型的值替换超类型的值场景,因此在刚刚定义泛型类的时候很难明确的。故仍是先从泛型类定义的内部特征着手会更加明确点。
假设二: 好比先根据泛型类内部定义一些方法和属性,因为刚开始定义并不能肯定是不是协变out仍是逆变in,因此上面的有无型变点不能做为断定条件,最开始还没肯定的时候通常当作不变泛型类来定义。
,最直白能够先看看型变点,而后根据型变点基本肯定泛型类内部表现特征,
补充一点,若是最终肯定是协变的,但是在定义的时候经过步骤1获得类型形参存在的位置处于函数形参位置,那么这时候就能够大胆借助@UnSafeVariance注解告诉编译器使得编译经过,逆变同理。
来张图理解下
是否还记得上一篇文章开头的那个例子和那幅漫画图
例子代码以下:
fun main(args: Array<String>) {
val stringList: List<String> = listOf("a", "b", "c", "d")
val intList: List<Int> = listOf(1, 2, 3, 4)
printList(stringList)//向函数传递一个List<String>函数实参,也就是这里List<String>是能够替换List<Any>
printList(intList)//向函数传递一个List<Int>函数实参,也就是这里List<Int>是能够替换List<Any>
}
fun printList(list: List<Any>) {
//注意:List是协变的,这里函数形参类型是List<Any>,函数内部是不知道外部传入是List<Int>仍是List<String>,所有当作List<Any>处理
list.forEach {
println(it)
}
}
复制代码
理解:
对于printList函数而言,它须要的是List<Any>
类型是个相对具体类型更加泛化的类型,且在函数内部的操做不会涉及到修改写操做,而后在外部传入一个更为具体的子类型确定是知足要求的泛化类型最基本需求。因此外部传入更为具体子类型List<String>、List<Int>
的兼容性更好。
例子代码以下:
class A<in T>{
fun doAction(t: T){
...
}
}
fun main(args: Array<String>) {
val intA = A<Int>()
val anyA = A<Any>()
doSomething(intA)//不合法,
doSomething(anyA)//合法
}
fun doSomething(a: A<Number>){//在doSomething外部不能传入比A<Number>更为具体的类型,由于在函数内部涉及写操做.
....
}
复制代码
理解:
对于doSomething,它须要的A<Number>
是个相对泛化类型更加具体的类型,因为泛型类A逆变的,函数内部的操做放开写操做权限,试着想下在doSomething函数外部不能传入比他更为具体的比较器对象了,由于只要有比A<Number>
更为具体的,就会出问题,利用反证法来理解下,假如传入A<Int>
类型是合法的,那么在内部函数仍是当作A<Number>
,在函数内部写操做时候颇有可能把它往里面写入一个Float
类型的数据,由于往Number类型写入Float类型是很合法的,可是外部实际上传入的是A<Int>
,往A<Int>
写Float类型不出问题才怪呢,因此原假设不成立。因此逆变放开了写权限,那么对于外部传入的类型要求就更加严格了。
引出另外一个问题,为何逆变写操做是安全的呢? 细想也是很简单的,对于逆变泛型类型做为函数形参的类型,那么在函数外部的传入实参类型就必定要比函数形参的类型更泛化不能更具体,因此在函数内部操做的最具体的类型也就是函数形参类型,因此确定能够大胆写操做啊。就好比A<Number>
类型形参类型,在doSomething函数中明确知道外部不能比它更为具体,因此在函数内部大胆在A<Number>
基础上写操做是能够的。
fun main(args: Array<String>) {
val stringList: MutableList<String> = mutableListOf("a", "b", "c", "d")
val intList: MutableList<Int> = mutableListOf(1, 2, 3, 4)
printList(stringList)//这里其实是编译不经过的
printList(intList)//这里其实是编译不经过的
}
fun printList(list: MutableList<Any>) {
list.add(3.0f)//开始引入危险操做dangerous! dangerous! dangerous!
list.forEach {
println(it)
}
}
复制代码
理解:
不变实际上就更好理解了,由于不存在子类型化关系,没有所谓的子类型A的值在任何地方任什么时候候能够替换超类型B的值的规则,因此上述例子编译不过,对于printList函数而言必须接收的类型是MutableList<Any>
,由于一旦传入和它不同的具体类型就会存在危险操做,出现不安全的问题。
因为篇幅缘由,因此星投影和协变、逆变实际例子的应用放到下一篇应用篇去了,可是到这里Kotlin泛型型变重点和难点已经所有讲完,后面一篇也就是实际开发中例子的运用。关于这篇文章仍是须要好好消化一下,最后再根据下一篇实际例子就能够更加巩固,下篇将会注重讲开发中的例子实现,不会再扣概念了。下篇敬请关注~~~
原创系列:
翻译系列:
实战系列:
欢迎关注Kotlin开发者联盟,这里有最新Kotlin技术文章,每周会不按期翻译一篇Kotlin国外技术文章。若是你也喜欢Kotlin,欢迎加入咱们~~~