写点什么

Kotlin 中的泛型:协变与逆变

作者:如浴春风
  • 2022 年 5 月 01 日
  • 本文字数:2885 字

    阅读完需:约 9 分钟

本文需要读者掌握的背景知识:

  • 对 Kotlin/Java 泛型或 C++ 模板具有一定了解。

  • 对 Java 字节码有一定认识。

前言

在学习 Kotlin,阅读官方源码时,常遇到如下形式的代码:

// 来自 kotlinx.coroutines// file: kotlinx-coroutines-core/common/src/internal/Scopes.ktinternal open class ScopeCoroutine<in T>(  context: CoroutineContext,  @JvmField val uCont: Continuation<T> // unintercepted continuation) : AbstractCoroutine<T>(context, true, true), CoroutineStackFrame {	// 省略……}
// 来自 kotlinx.coroutines// file: kotlinx-coroutines-core/common/src/Deferred.ktpublic interface Deferred<out T>: Job { // 省略……}
复制代码

根据对其他编程语言的经验,不难理解这里用到了模版/泛型的概念。但是,关键字 inout 又是什么含义,出现在泛型类型占位符的前面有什么作用?

泛型(Generics)

什么是泛型

泛型的本质是参数化类型,即把类型当作参数。

以求数组最大值为例,

  • 如果没有泛型,我们需要为每个数值类型写一个函数

fun getMaxElement(array: Array<Int>): Int? = array.maxByOrNull { it }
fun getMaxElement(array: Array<Float>): Float? = array.maxByOrNull { it }
fun getMaxElement(array: Array<Double>): Double? = array.maxByOrNull { it }
复制代码

显而易见,这种方式带来了很多重复的代码。经分析发现规律:上述三个函数,作用相同,都是比较数值的大小,仅传入和返回的参数类型不同

如果能写成 fun getMaxElement(array: Array<T>): T? = array.maxByOrNull { it } 这样的代码,就好了。省去了很多重复且不必要的代码。泛型是为了解决这个问题的。

  • 有了泛型后,对相同功能、但参数类型不同的函数,只需要写一个函数

fun <T> getMaxElement(array: Array<T>): T? = array.maxByOrNull { it }
复制代码

对容器(如 ListArray)、类(Class),也是同样的道理。

泛型如何实现

先来看个例子:现在有两个 List,以及它们对应的反汇编代码(Kotlin Bytecode)

// val listA = listOf<String>()INVOKESTATIC kotlin/collections/CollectionsKt.emptyList ()Ljava/util/List;ASTORE 0
// val listB = listOf<Int>()INVOKESTATIC kotlin/collections/CollectionsKt.emptyList ()Ljava/util/List;ASTORE 1
复制代码

可以看到它们几乎毫无区别,那它们是怎么实现泛型的特性呢?


Kotlin 是一门在 JVM 上运行的静态类型编程语言,所以我们可以参考下 Java 关于泛型的实现。

通过搜索资料发现(参考资料 1),Java 是通过类型擦除(Type erasure)实现泛型的。

步骤:

  • 编译产生字节码时,用 Object(针对没有约束的泛型)或者约束类型(针对有约束的泛型)替换泛型参数。

  • 为了类型安全,必要时插入类型转换。

  • 为保留泛型的多态性,生成 Bridge Methods

可以看到,过程中未产生新的 Class,并且均是在编译期完成的,所以泛型的引入未增加运行时开销。


但为什么要这么做?

部分原因是 Java 的向后兼容。泛型在 Java 1.5 引入的,在它之前,要实现类似的功能,是通过强制类型转换,所以底层存储用的是 Object 对象。为了兼容这点,泛型未对底层存储数据类型进行改革,而是通过编译器检查类型、自动转换类型等技术实现。

举例:本节开头的例子中,只定义了两个分别存储 StringIntList,通过反编译查看字节码发现它们在对象创建时的指令是相同的,那访问元素时呢?

// listA[0] + ""ALOAD 0ICONST_0INVOKEINTERFACE java/util/List.get (I)Ljava/lang/Object; (itf)CHECKCAST java/lang/StringLDC ""INVOKESTATIC kotlin/jvm/internal/Intrinsics.stringPlus (Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/String;POP
// listB[0] + 1ALOAD 1ICONST_0INVOKEINTERFACE java/util/List.get (I)Ljava/lang/Object; (itf)CHECKCAST java/lang/NumberINVOKEVIRTUAL java/lang/Number.intValue ()IICONST_1IADD
复制代码

可以看出,从 List 中取出元素时仍是 Object 类型,随后编译器会插入类型转换指令,并继续进行操作。

泛型的优点

总结下,Kotlin/Java 实现的泛型具有如下优点:

  1. 消除重复代码。通过将类型参数化,大大减少重复代码。

  2. 类型安全。编译器在编译期完成类型的检测与转换。

泛型的变种(Variance)

协变(Covariant):out

定义:如果类型 A 是类型 B 的子类型,那么类型 Generics<A> 也是 Generics<B> 的子类型。


举例:StringObject 的子类型(显而易见),那么协变是指 List<String> 也是 List<Object> 的子类型。


Java 中为保证运行时类型安全,默认是没有这种关系的。但这种关系又很实用,例如元素拷贝:

void copyAll(Collection<Object> to, Collection<String> from) {	to.addAll(from);}
复制代码

如果没有协变,上述用法会报错。所幸 Java 支持这个,查看下 Collection.addAll() 方法签名:

interface Collection<E> extends Iterable<E> {  boolean addAll(Collection<? extends E> c);}
复制代码


Java 通过类型参数 ? extends E 实现协变关系。在上面的例子中,它表示 addAll 接受 Collection<E>Collection<T> 作为函数的传入参数类型,其中 T 代表 E 的子类型(subtype)。Kotlin 中通过关键字 out 实现这种关系

public interface Collection<out E> : Iterable<E> {	// 省略……}
public interface MutableCollection<E> : Collection<E>, MutableIterable<E> { public fun addAll(elements: Collection<E>): Boolean}
复制代码

逆变(Contravariance):in

定义:如果类型 A 是类型 B 的子类型,那么逆变就是类型 Generics<B> 是 Generics<A> 的子类型。跟协变是反过来的


举例:StringObject 的子类型,那么逆变是指 List<Object> 是指 List<String> 的子类型。

Java 中通过 <? super T> 实现这种关系。Kotlin 中通过关键字 in 实现这种关系

应用场景

脱离应用场景的编程技巧是刷流氓的。


为什么需要逆变、协变?为什么有协变后,还需要逆变?


考虑一个泛型生产者 Producer<T>,它只生产不消费,即它的方法中,没有任何一个方法的传入参数具有类型 T,仅存在返回类型 T 的方法。这种情况下,使用 Producer<T> 引用一个 Producer<E> 的实例无疑是安全的,其中类型 E 是类型 T 的子类型。如果没有协变,则不支持这种操作

interface Producer<out T> {	suspend fun produce(): T}
interface Job { fun execute()}
class MainJob : Job { override fun execute() { // TODO }}
class JobProducer: Producer<MainJob> { override fun produce(): MainJob { return MainJob() }}
// OK,无语法错误、无编译报错val produce: Producer<Job> = JobProducer()
复制代码


与协变相反,逆变只能被消费,不能被生产

interface Comparable<in T> {	operator fun compareTo(other: T>): Int}
fun test(comparable: Comparable<Number>) { comparable.compareTo(1.0) // OK,Comparable<Number> -> Comparable<Double>, // 由于有 in 修饰,不会报错 val y: Comparable<Double> = comparable}
复制代码

总结

参考资料

  1. Lesson: Generics (Updated) -- The Java™ Tutorials

  2. Generics: in, out, where

发布于: 刚刚阅读数: 3
用户头像

如浴春风

关注

还未添加个人签名 2020.02.29 加入

某Top Android手机厂商,相机开发工程师一枚

评论

发布
暂无评论
Kotlin 中的泛型:协变与逆变_5月月更_如浴春风_InfoQ写作社区