教你如何攻克Kotlin中泛型型变的难点(实践篇)

简述: 这是泛型型变最后一篇文章了,也是泛型介绍的最后一篇文章。顺便再扯点别的,上周去北京参加了JetBrains 2018开发者日,主要是参加Kotlin专场。我的感受收获仍是挺多的,bennyHuo和彦伟老师精彩演讲确实传递不少干货啊,固然还有Hali布道师大佬带来了的Kotlin1.3版本的新特性以及Google中国技术推广负责人钟辉老师带来的Coroutines在Android开发中的应用。因此准备整理以下几篇文章为后续发布:java

  • 一、Kotlin中1.3版本新特性都有哪些?
  • 二、Kotlin中的Coroutine(协程)在Android上应用(协程学前班篇)
  • 三、Ktor异步框架初体验(Ktor学前班篇)
  • 四、Kotlin中data class的使用(benny大佬在大会上讲的很清楚了,也很全面。主要讲下我的以前踩过的坑,特别是用于后端开发坑更多)

那么今天这篇文章主要是为了给上篇型变文章两个尾巴以及泛型型变是如何被应用到实际开发中的去。而且我会用上篇博客如何去选择相应型变的方法一步步肯定最终咱们该使用协变、逆变、仍是不变,我会用一个实际例子来讲明。这篇文章比较简单主要就如下四点:后端

  • 一、Kotlin声明点变型与Java中的使用点变型进行对比
  • 二、如何使用Kotlin中的使用点变型
  • 三、Kotlin泛型中的星投影
  • 四、使用泛型型变实现可用于实际开发中的Boolean扩展

1、Kotlin声明点变型与Java中的使用点变型进行对比

一、声明点变型和使用点变型定义区别

首先,解释下什么是声明点变型和使用点变型,声明点变型顾名思义就是在定义声明泛型类的时候指明型变类型(协变、逆变、不变),在Kotlin上表现形式就是在声明泛型类时候在泛型形参前面加in或out修饰。使用点变型就是在每次使用该泛型类的时候都要去明确指出型变关系,若是你对Java中型变熟悉的话,Java就是使用了使用点变型.安全

二、二者优势对比

声明点变型:app

  • 有个明显优势就是只须要在泛型类声明时定义一次型变对应关系就能够了,那么以后无论在任何地方使用它都不用显示指定型变对应关系,而使用点变型就是每处使用的地方都得重复定义一遍特别麻烦(又找到一处Kotlin优于Java的地方)。

使用点变型:框架

  • 实际上使用点变型也是有使用场景的,可使用的更加灵活;因此Kotlin并无彻底摒弃这个语法点,下面会专门介绍它的使用场景。

三、使用对比

刚刚说使用点变型特别麻烦,一块儿来看看到底有多麻烦。这里就是以Java为表明,咱们都知道Java中要使用型变,是利用?通配符加(super/extends)来达到目的,例如: Function<? super T, ? extends E>, 其中的? extends E就是对应了协变,而? super T对应的是逆变。这里以Stream API中的flatMap函数源码为例异步

@FunctionalInterface
public interface Function<T, R> {//声明处就不用指定型变关系
    ...
}

//能够看到使用点变型很是麻烦,定义一个mapper的Function泛型类参数时,还须要指明后面一大串Function<? super T, ? extends Stream<? extends R>>
  <R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
复制代码

声明点变型到底有多方便,这里就以Kotlin为例,Kotlin使用in, out来实现型变对应规则。这里以Sequences API中的flapMap函数源码为例ide

public interface Sequence<out T> {//Sequence定义处声明了out协变
    /** * Returns an [Iterator] that returns the values from the sequence. * * Throws an exception if the sequence is constrained to be iterated once and `iterator` is invoked the second time. */
    public operator fun iterator(): Iterator<T>
}

public fun <T, R> Sequence<T>.flatMap(transform: (T) -> Sequence<R>): Sequence<R> {//能够看到因为Sequence声明了协变,因此flatMap函数Sequence中的泛型实参R就不用再次指明型变类型了
    return FlatteningSequence(this, transform, { it.iterator() })
}
复制代码

经过以上源码对比,明显看出Kotlin中的声明点变型要比Java中的使用点变型要简单得多吧。可是呢使用点变型并非一无可取,它在Kotlin中仍是有必定的使用场景的。下面即将揭晓函数

2、如何使用Kotlin中的使用点变型

实际上使用点变型在Kotlin中仍是有必定的使用场景,想象一下这样一个实际场景,尽管某个泛型类是不变的,也就是具备可读可写的操做,但是有时候在某个函数中,咱们通常仅仅只用到只读或只写操做,这时候利用使用点变型它能使一个不变型的缩小型变范围蜕化成协变或逆变的。是否是忽然懵逼了,用源码来讲话,你就明白了,一块儿来看个源码中的例子。post

Kotlin中的MutableCollection<E>是不变的,一块儿来看了下它的定义ui

public interface MutableCollection<E> : Collection<E>, MutableIterable<E> {//没有in和out修饰,说明是不变
    override fun iterator(): MutableIterator<E>
    public fun add(element: E): Boolean
    public fun remove(element: E): Boolean
    public fun addAll(elements: Collection<E>): Boolean
    public fun removeAll(elements: Collection<E>): Boolean
    public fun retainAll(elements: Collection<E>): Boolean
    public fun clear(): Unit
}
复制代码

而后咱们接着看filter和filterTo函数的源码定义

public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
    return filterTo(ArrayList<T>(), predicate)
}

//注意: 这里<T, C : MutableCollection<in T>>, MutableCollection<in T>声明成逆变的了,是否是很奇怪啊,以前明明有说它是不变的啊,怎么这里就声明逆变了
public inline fun <T, C : MutableCollection<in T>> Iterable<T>.filterTo(destination: C, predicate: (T) -> Boolean): C {
    for (element in this) if (predicate(element)) destination.add(element)
    return destination
}
复制代码

经过上面的函数是否是发现和MutableCollection不变相违背啊,实际上不是的。这里就是一种典型的使用点变型的使用,咱们能够再仔细分析下这个函数,destination在filterTo函数的内部只作了写操做,遍历Iterable中的元素,并把他们add操做到destination集合中,能够验证咱们上述的结论了,虽然MutableCollection是不变的,可是在函数内部只涉及到写操做,彻底就可使用 使用点变型将它指定成一个逆变的型变类型,由不变退化成逆变明显不会影响泛型安全因此这里处理是彻底合法的。能够再去看其余集合操做API,不少地方都使用了这种方式。

上述关于不变退化到逆变的,这里再讲个不变退化到协变的例子。

//能够看到source集合泛型类型声明成了out协变了???
fun <T> copyList(source: MutableList<out T>, destination: MutableList<T>): MutableList<T>{
    for (element in source) destination.add(element)
}
复制代码

MutableList<E>就是前面常说的不变的类型,一样具备可读可写操做,可是这里的source的集合泛型类型声明成了out协变,会不会又蒙了。应该不会啊,有了以前逆变的例子,应该你们都猜到为何了。很简单就是由于在copyList函数中,source集合没有涉及写操做只有读操做,因此可使用 使用点变型将MutableList的不变型退化成协变型,并且很显然不会引入泛型安全的问题。

因此通过上述例子和之前例子关于如何使用逆变、协变、不变。仍是我以前说那句话,不要去死记规则,关键在于使用场景中读写操做是否引入泛型类型安全的问题。若是明确读写操做的场景了彻底能够按照上述例子那样灵活运用泛型的型变的,能够程序写得更加完美。

3、Kotlin泛型中的星投影

一、星投影的定义

星投影是一种特殊的星号投影,它通常用来表示不知道关于泛型实参的任何信息,换句话说就是它表示一种特定的类型,可是只是这个类型不知道或者不能被肯定而已。

二、MutableList<*>MutableList<Any?>区别

首先咱们须要注意和明确的一点就是MutableList<*>MutableList<Any?>是不同的,MutableList<*>表示包含某种特定类型的集合;而MutableList<Any?>则是包含任意类型的集合。特定类型集合只不过不太肯定是哪一种类型,任意类型表示包含了多种类型,区别在于特定集合类型一旦肯定类型,该集合只能包含一种类型;而任意类型就能够包含多种类型了。

三、MutableList<*>实际上一个out协变投影

MutableList<*>其实是投影成MutableList<out Any?>类型

首先,咱们来分析下为何会这样投影,咱们知道MutableList<*>只包含某种特定类型的集合,多是String、Int或者其余类型中的一种,可想而知对于该集合操做须要禁止写操做,不能往该集合中写入数据,由于没法肯定该集合的特定类型,写操做极可能引入一个不匹配类型到集合中,这是一件很危险的事。可是反过来想下,若是该集合存在只读操做,读出数据元素类型虽然不知道,可是始终是安全的。只存在读操做那么说明是协变,协变就会存在保留子类型化关系,也就是读出数据元素类型是不肯定类型子类型,那么可想而知它只替换Any?类型的超类型,由于Any?是全部类型的超类型,那么保留型化关系,因此MutableList<*>实际上就是MutableList<out Any?>的子类型了。

4、使用泛型型变实现可用于实际开发中的Boolean扩展

关于Boolean扩展的实现,主要来源于看了BennyHuo大佬写的一些代码中发现的,原来能够这么方便的写if-else,因而乎就去看了下它的实现 可能不少人都知道了它的实现,为何要讲这个由于这是Kotlin泛型协变实际应用一个很是不错的例子。

一、为何开发一个Boolean扩展

给出一个例子场景,判断一堆数集合中是否全是奇数,若是全是返回输出"奇数集合",若是不是请输出"不是奇数集合"

首先问下你们是否写过一下相似下面代码

//java版写法

public void isOddList(){
    int count = 0;
    for(int i = 0; i < numberList.size(); i++){
        if(numberList[i] % 2 == 1){
            count++;
        }
    }
    if(count == numberList.size()){
       System.out.println("奇数集合");
       return;
    }
    System.out.println("不是奇数集合");
}

复制代码
//kotlin版写法

fun isOddList() = println(if(numberList.filter{ it % 2 == 1}.count().equals(numberList.size)){"奇数集合"} else {"不是奇数集合"})
复制代码
//Boolean扩展版本写法
fun isOddList() = println(numberList
          .filter{ it % 2 == 1 }
          .count()
          .equals(numberList.size)
          .yes{"奇数集合"}
          .otherwise{"不是奇数集合"})//有没有发现Boolean扩展这种链式调用更加丝滑
复制代码

对比发现,虽然Kotlin中的if-else表达式自带返回值的,可是if-else的结构会打断链式调用,可是若是使用Boolean扩展,彻底可使你的链式调用更加丝滑顺畅一路调用到底。

二、Boolean扩展使用场景

Boolean扩展的使用场景我的认为有两个:

  • 配合函数式API一块儿使用,遇到if-else判断的时候建议使用Boolean扩展,由于它不会像if-else结构同样会打断链式调用的结构。
  • 另外一场景就是if的判断条件组合不少,若是在外层再包裹一个if代码显得更加臃肿了,此时使用Boolean会使代码更简洁。

三、Boolean代码实现

经过观察上述Boolean扩展的使用,咱们首先须要明确几点:

  • 第一点:咱们知道yes、otherwise实际上就是两个函数,为何能链式连接起来讲明中间确定有一个相似桥梁做用的中间类型做为函数的返回值类型。
  • 第二点:yes、otherwise函数的做用域是带返回值的,例如上述例子它能直接返回字符串类型的数据。
  • 第三点: yes、oterwise函数的都是一个lamba表达式,而且这个lambda表达式将最后表达式中的值返回
  • 第四点: yes函数是在Boolean类型调用,因此须要基于Boolean类型的实现扩展函数

那么根据以上得出几点特征基本能够把这个扩展的简单版本写出来了(暂时不支持带返回值的)

//做为中间类型,实现链式连接
sealed class BooleanExt 
object Otherwise : BooleanExt()
object TransferData : BooleanExt()

fun Boolean.yes(block: () -> Unit): BooleanExt = when {
    this -> {
        block.invoke()
        TransferData//因为返回值是BooleanExt,因此此处也须要返回一个BooleanExt对象或其子类对象,故暂且定义TransferData object继承BooleanExt
    }
    else -> {//此处为else,那么须要连接起来,因此须要返回一个BooleanExt对象或其子类对象,故定义Otherwise object继承BooleanExt
        Otherwise
    }
}

//为了连接起otherwise方法操做因此须要写一个BooleanExt类的扩展
fun BooleanExt.otherwise(block: () -> Unit) = when (this) {
    is Otherwise -> block.invoke()//判断此时子类,若是是Otherwise子类执行block
    else -> Unit//不是,则直接返回一个Unit便可
}


fun main(args: Array<String>) {
    val numberList: List<Int> = listOf(1, 2, 3)
    //使用定义好的扩展
    (numberList.size == 3).yes {
        println("true")
    }.otherwise {
        println("false")
    }
}
复制代码

上述的简单版基本上把扩展的架子搭出来可是呢,惟一没有实现返回值的功能,加上返回值的功能,这个最终版本的Boolean扩展就实现了。

如今来改造一下原来的版本,要实现返回值那么block函数不能再返回Unit类型,应该要返回一个泛型类型,还有就是TransferData不能使用object对象表达式类型,由于须要利用构造器传入泛型类型的参数,因此TransferData用普通类替代就行了。

关因而定义成协变、逆变仍是不变型,咱们能够借鉴上篇文章使用到流程选择图和对比表格

将从基本结构形式、有无子类型化关系(保留、反转)、有无型变点(协变点out、逆变点in)、角色(生产者输出、消费者输入)、类型形参存在的位置(协变就是修饰只读属性和函数返回值类型;逆变就是修饰可变属性和函数形参类型)、表现特征(只读、可写、可读可写)等方面进行对比

协变 逆变 不变
基本结构 Producer<out E> Consumer<in T> MutableList<T>
子类型化关系 保留子类型化关系 反转子类型化关系 无子类型化关系
有无型变点 协变点out 逆变点in 无型变点
类型形参存在的位置 修饰只读属性类型和函数返回值类型 修饰可变属性类型和函数形参类型 均可以,没有约束
角色 生产者输出为泛型形参类型 消费者输入为泛型形参类型 既是生产者也是消费者
表现特征 内部操做只读 内部操做只写 内部操做可读可写

  • 第一步:首先根据类型形参存在位置以及表现特征肯定
sealed class BooleanExt<T>

object Otherwise : BooleanExt<Any?>()

class TransferData<T>(val data: T) : BooleanExt<T>()//val修饰data

inline fun <T> Boolean.yes(block: () -> T): BooleanExt<T> = when {//T处于函数返回值位置
    this -> {
        TransferData(block.invoke())
    }
    else -> Otherwise//注意: 此处是编译不经过的
}

inline fun <T> BooleanExt<T>.otherwise(block: () -> T): T = when (this) {//T处于函数返回值位置
    is Otherwise ->
        block()
    is TransferData ->
        this.data
}
复制代码

经过以上代码咱们能够基本肯定是协变或者不变,

  • 第二步:判断是否存在子类型化关系

因为yes函数else分支返回的是Otherwise编译不经过,很明显此处不是不变的,由于上述代码就是按照不变方式来写的。因此基本肯定就是协变。

而后接着改,首先将sealed class BooleanExt<T>改成sealed class BooleanExt<out T>协变声明,而后发现Otherwise仍是报错,为何报错啊,报错缘由是由于yes函数要求返回一个BooleanExt<T>类型,而此时返回Otherwise是个BooleanExt<Any?>(),反证法,假如上述是合理,那么也就是BooleanExt<Any?>要替代BooleanExt<T>出现的地方,BooleanExt<Any?>BooleanExt<T>子类型,因为BooleanExt<T>协变的,保留子类型型化关系也就是Any?T子类型,明显不对吧,咱们都知道Any?是全部类型的超类型。因此原假设明显不成立,因此编译错误很正常,那么逆向思考下,我是否是只要把Any?位置用全部的类型的子类型Nothing来替换不就符合了吗,那么咱们天然而然就想到Nothing,在Kotlin中Nothing是全部类型的子类型。因此最终版本Boolean扩展代码以下

sealed class BooleanExt<out T>//定义成协变

object Otherwise : BooleanExt<Nothing>()//Nothing是全部类型的子类型,协变的类继承关系和泛型参数类型继承关系一致

class TransferData<T>(val data: T) : BooleanExt<T>()//data只涉及到了只读的操做

//声明成inline函数
inline fun <T> Boolean.yes(block: () -> T): BooleanExt<T> = when {
    this -> {
        TransferData(block.invoke())
    }
    else -> Otherwise
}

inline fun <T> BooleanExt<T>.otherwise(block: () -> T): T = when (this) {
    is Otherwise ->
        block()
    is TransferData ->
        this.data
}
复制代码

5、结语

到这里Kotlin中有关泛型的全部文章就结束,固然泛型很重要深刻于实际开发各个地方,特别是开发一些框架东西比较多,能够看到上述Boolean实现就是按照上篇文章教你如何攻克Kotlin中泛型型变的难点(下篇)规则来决定使用哪一种型变类型以及稍加分析下就出来了。总的来讲有了那张图作指导仍是很方便的。其实关于泛型型变,仍是得须要多理解,不能死记规则,只有这样才能更加灵活运用。最后很是感谢bennyHuo大佬提供的Boolean扩展实现。

Kotlin系列文章,欢迎查看:

原创系列:

翻译系列:

实战系列:

欢迎关注Kotlin开发者联盟,这里有最新Kotlin技术文章,每周会不按期翻译一篇Kotlin国外技术文章。若是你也喜欢Kotlin,欢迎加入咱们~~~

相关文章
相关标签/搜索