章节索引 :

Kotlin 泛型型变的应用

上篇文章我们一起研究了 Kotlin 泛型中型变,而这篇文章主要是为了补充泛型中的星投影以及泛型型变是如何被应用到实际开发中的去。并且我会用上篇文章知识一步步确定最终我们该使用协变、逆变、还是不变,在最后会用一个实际例子来说明。

1. 声明点变型与使用点变型进行对比

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

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

1.2 两者优点对比

声明点变型:

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

使用点变型:

实际上使用点变型也是有使用场景的,可以使用的更加灵活;所以 Kotlin 并没有完全摒弃这个语法点,下面会专门介绍它的使用场景。

1.3 使用对比

刚刚说使用点变型特别麻烦,一起来看看到底有多麻烦。这里就是以 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 函数源码为例:


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 中还是有一定的使用场景,想象一下这样一个实际场景,尽管某个泛型类是不变的,也就是具有可读可写的操作,可是有时候在某个函数中,我们一般仅仅只用到只读或只写操作,这时候利用使用点变型它能使一个不变型的缩小型变范围蜕化成协变或逆变的。是不是突然懵逼了,用源码来说话,你就明白了,一起来看个源码中的例子。

Kotlin 中的 MutableCollection<E> 是不变的,一起来看下它的定义:

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 泛型中的星投影

3.1 星投影的定义

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

3.2 MutableList<*>MutableList<Any?> 区别

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

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

MutableList<*> 实际上是投影成 MutableList<out Any?> 类型:

我们来分析下为什么会这样投影,我们知道 MutableList<*> 只包含某种特定类型的集合,可能是 String、Int 或者其他类型中的一种,可想而知对于该集合操作需要禁止写操作,不能往该集合中写入数据,无法确定该集合的特定类型,写操作很可能引入一个不匹配类型到集合中,这是一件很危险的事。

但是反过来想下,如果该集合存在只读操作,读出数据元素类型虽然不知道,但是始终是安全的。只存在读操作那么说明是协变,协变就会存在保留子类型化关系,也就是读出数据元素类型是不确定类型子类型,那么可想而知它只替换 Any? 类型的超类型,因为 Any? 是所有类型的超类型,那么保留型化关系,所以 MutableList<*> 实际上就是 MutableList<out Any?> 的子类型了。

4. 一个实际例子应用泛型型变 (Boolean 扩展)

4.1 为什么开发一个 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 扩展,完全可以使你的链式调用更加丝滑顺畅一路调用到底。

4.2 Boolean 扩展使用场景

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

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

4.3 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 中有关泛型的所有内容就结束了,当然泛型很重要,可以深入于实际开发各个方面,特别是开发一些框架的时候用的比较多。其实关于泛型型变,还是得需要多理解,不能死记规则,只有这样才能更加灵活运用。