“你是完全不懂泛型吗?” 从0开始,读懂Kotlin泛型的官方文档

是谁,面试被问到Kotlin泛型,张口结舌半天只能说出“emmm泛型就是很宽泛的类型”......而等我跑去看了Kotlin的官方文档,才发现自己还真是完全不懂泛型——已经连官方文档都看不懂了(҂⌣̀_⌣́)

所以本篇是从完全不理解泛型的基础出发的,整体思路是从Kotlin泛型的官方文档入手,并参考相关博客去理解其中提到的知识点,依然会很基础+琐碎。如有理解错误,期盼指正。

相关链接:

 一、泛型的定义与基础用法

泛型类

Kotlin文档对泛型的基本介绍并不多,只简单给出了泛型类泛型函数的使用示例。(除了类与函数之外,Kotlin也支持泛型接口,它的使用与泛型类类似,这里就不额外介绍了。)示例中,泛型类与泛型函数分别在类名之后函数名之前定义类型参数 <T> ,并在创建类与调用函数时指定或推断这个类型参数。

而对于一个完全不懂泛型的人来说,看了这个介绍之后依然是一头雾水:所以泛型到底是什么?类型参数又是什么意思?<T>和 T 又都代表着什么呢?

1、什么是泛型?

简单来说,泛型就是参数化类型

“参数化”乍一看可能会有些莫名,但其实很容易理解。只需要联想一下常规的有参函数,例如,一个像 funName(param1, param2) 这样的函数,param1和param2就是这个函数和函数体被定义时使用的两个参数的名字或者说标识符,也叫形参,这些形参会在且只在函数体内充当占位符,等函数被调用并一一对应地传入两个参数真实的、确定的值,即实参时,占位符才被真正地赋值。参数使得同一个函数体可以处理不同的输入,进而实现了代码的复用与功能上的扩展。

在此基础上,就可以浅浅地理解“参数化类型”了:把类型当作一个参数那样定义与指定。而这样的一个参数就叫作类型参数,它在定义阶段只被表示为一个标识符即类型形参,并在它的作用域内充当一个类型的占位符,等到调用时才会被赋予真实的类型值,即类型实参

如此,类型参数便也像传统的参数那样,使得同一个泛型类/接口/函数可以处理不同的类型,实现代码的复用与功能的扩展。

接下来就一边介绍泛型的基础用法,一边更直观地了解类型的参数化是如何体现的。

2、泛型的基础使用


首先泛型的使用要从理解 <T> 开始

<T> 可以说是泛型中最常见的一个表述了,我甚至一度误以为这是一个声明泛型的固定搭配,仔细了解后才知道<>与 T 其实有它们各自独立的含义。

  • 尖括号<>:个人理解,就像函数定义时的形参列表和调用时的实参列表要用括号()包裹那样,泛型使用尖括号<>来包裹类型参数的形参列表与实参列表。当在定义中使用 <T> 时,实际上是声明了一个类型形参列表,这个列表只有一个形参 T;相应的,在调用阶段,需要在 <> 中一一对应地指定实参,那么形参列表 <T> 就可以对应一个实参列表例如 <String>。所以,可以声明泛型的只是符号 <>——它声明了一个类型参数列表;
  • T:按照上面的理解,那么 T 的角色其实只是一个类型参数的形参,换句话说,它只是一个名字或者标识符,并没有这之外的作用;而它之所以使用最为广泛,则是出于一种约定俗成,像泛型中用 T 表示 Type,K、V 表示 Key 和 Value,E 表示 Element 这些约定,显然是可以大大提高代码的规范性与可读性的。

了解以上两点后,就可以尝试自己定义并创建一个泛型类,代码如下:

class GenericClass<GENERIC, X, Y, Z> (val prop: GENERIC){
}

val genClass = GenericClass<String, Int, Int, Int>("泛型")  
// 也可以用_代替String
// val genClass = GenericClass<_, Int, Int, Int>("泛型")  

可以看到,要定义一个泛型类,只需要在类名之后用 <> 声明一个类型形参的列表,而参数个数与命名其实是没有特别严格的限制的;等创建这个泛型类的实例时,就同样用 <> 传入一个跟定义一一对应的类型实参列表,如果类型实参可以被推断出来,那么也可以省略或者用下划符_替代。

然后,可以进一步理解类型形参作为占位符的作用域

就像常规的有参函数在函数体中使用形参作为占位符那样,当泛型声明好一个类型形参列表之后,就可以在作用域里使用这些类型形参来占位表示类型。

对于泛型类与泛型接口,类型参数声明在类名与接口名之后,作用域是整个类与接口,包括主构造函数、继承、实现、类/接口内部等所有位置。

以接口MutableList的源码为例,除了 MutableList<E> 中的 E 是声明了一个类型参数外,之后出现的所有 E 就都是占位符。

public interface MutableList<E> : List<E>, MutableCollection<E> {
    ...
    override fun add(element: E): Boolean
    ...
    override fun addAll(elements: Collection<E>): Boolean
    ...
}

泛型函数也是类似。

以上面提过的泛型函数 fun <T> singletonList(item: T): List<T> fun <T> T.basicToString(): String为例,可以看到泛型函数的类型参数声明在函数名之前,函数的参数列表、返回值、函数体还有扩展函数的接收者都可以使用已声明的类型形参。(所以JAVA的类型参数声明在返回值之前)

在此基础上,就很好理解一种比较容易令人迷惑的场景了,那就是泛型类/接口中的泛型方法

如下展示了一个例子,在声明了一个类型形参 T 的泛型类中,再定义一个声明了类型形参 T 的泛型函数,那么这个泛型函数中的 T 和泛型类中的 T 是什么关系?

答案是没有关系

因为首先,泛型函数名之前的 <T> 不是使用泛型类的占位符,而是声明它自己的一个类型参数 T ,这个 T 只可以由调用函数时传入的实参指定;然后,根据作用域的就近原则,泛型函数中的所有占位符 T 就都指向泛型函数自己的类型参数 T,而与泛型类无关了。

class GenericClass<T> (val prop: T){
    fun funInGClass(funProp: T) {
        // 普通函数内的T,是泛型类的类型参数T
    }
    fun <T> gFunInGClass(funProp: T) {
        // 就近原则,泛型函数内的T是函数的类型形参,而不是类的类型形参
    }
}

val genClass = GenericClass<String>("泛型")
genClass.gFunInGClass(1)   // 不报错   推断泛型函数的类型实参为Int
genClass.funInGClass(1)    // 报错     expected type String

 二、泛型的型变:从JAVA到Kotlin

对泛型具备基础了解之后,就继续查看Kotlin文档。

没错,Kotlin的泛型文档几乎是一上来便开始讲型变了,不太容易理解,所以这一部分就先简单介绍一下型变的基础概念,再按照文档的结构安排逐段介绍。

1、型变是什么?

型变Variance是一个用来描述“类型构造器如何根据内部类型关系组成自己的类型关系”的概念,它并不是针对泛型或者某一种特定的语言。如果一个类型构造器保持了内部类型的序关系,那么它就是协变的;反之如果逆转了序关系,那么它便是逆变的;如果两种都不适用,那它就是不变的。
    
例如有一个类A与类B,其中A是B的子类(这意味着A可以胜任B的任何场景)。又有一个类型构造器 X<>。如果 X 对它的内部类型只有抛出操作,即如果调用 X<B>是为了抛出一个B的话,那么其实 X<A> 抛出的A也可以胜任,也就是说 X<A> 可以胜任 X<B> 的场景,即 X<A> 总是 X<B> 的子类,那就可以说 X 是协变的;如果 X 对它的内部类型只有接收操作,即调用 X<A> 是为了接收一个A的话,那么接收B的 X<B> 其实也可以接收这个A,也就是说 X<B> 可以胜任 X<A> 的场景,即 X<B> 总是 X<A> 的子类,那就可以说 X 是逆变的。如果 X 对内部类型既有抛出操作又有接收操作,那么它就应该是不变的,否则会有不安全的操作。
    
虽然会有些绕,但其实是很直观的概念。

2、JAVA 中的型变与通配符 

首先,Java 中的泛型是不型变的, 这意味着 List<String> 并不是 List<Object> 的子类型。如果 List 不是不型变的,它就没比 Java 的数组好到哪去,因为如下代码会通过编译但是导致运行时异常:

// 注意这段代码只是一种“如果”,实际是无法通过编译的
List<String> strs = new ArrayList<String>();
List<Object> objs = strs;
objs.add(1);
String s = strs.get(0);


 · JAVA泛型 不型变
    

了解了型变的基础概念后,泛型的不型变就容易理解了。
    
首先,泛型中的型变概念应该是针对泛型类与泛型接口的,即将泛型类与泛型接口看作是类型构造器,将类型参数看作是内部类型

泛型不型变,也就意味着,类型实参之间的父子关系,并不能使它们对应的类/接口得以胜任彼此的场景,就像示例代码中提到的 List<String>List<Object>,因为 List<T> 作为一个容器既有抛出操作又有接收操作,所以 List<String> 无法胜任 List<Object> 接收其它子类例如Int的场景。


· 补充:为什么会提到JAVA数组

如果 List 不是不型变的,它就没比 Java 的数组好到哪去。

为什么会这么说呢?我在JAVA中实验了一下后,发现JAVA数组确实是允许将一个父类数组指向一个子类数组,并允许传入一个其它子类的对象,进而造成编译时通过、而运行时报错的问题。

// 定义一个父类Parent,和两个子类Child、ChildA
class Parent {}
class Child extends Parent {}
class ChildA extends Parent{}


Parent[] pArr;                  // Parent数组
Child[] cArr = new Child[5];    // Child数组
pArr = cArr;                    // Parent数组可指向Child数组
pArr[0] = new ChildA();         // Parent数组可以传入一个ChildA对象  
                                // 运行时抛出异常:ArrayStoreException 类型不兼容


也就是说,不型变的泛型 List<T> 是不允许类似的代码通过编译的,因此比JAVA数组更优。
    
这里也能显示出泛型除了代码复用与功能扩展外的另一个重要作用:类型安全——泛型会在编译期间进行类型检查与类型转换。
    

· JAVA泛型 上界通配符与协变

Java 禁止这样的事情(指型变)以保证运行时的安全。但这样会有一些影响。例如, 考虑 Collection 接口中的 addAll() 方法。该方法的签名应该是什么?直觉上, 需要这样写:void addAll(Collection<E> items); 但随后,就无法做到以下这样(完全安全的)的事:

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

这就是为什么 addAll() 的实际签名是以下这样:

interface Collection<E> …… {
    void addAll(Collection<? extends E> items);
}

通配符类型参数 ? extends E 表示此方法接受 E 或者 E 的一个子类型对象的集合,而不只是 E 自身。 这意味着我们可以安全地从其中 (该集合中的元素是 E 的子类的实例)读取 E,但不能写入, 因为我们不知道什么对象符合那个未知的 E 的子类型。 反过来,该限制可以得到想要的行为:Collection<String> 表示为 Collection<? extends Object> 的子类型。 简而言之,带 extends 限定(上界)的通配符类型使得类型是协变的(covariant)。

Kotlin文档接着介绍了JAVA的上界通配符 <? extends E> 

  • 通配符 ? 与 ? extends E

首先,? 是JAVA提供的一个类型通配符,用来指代任何未知的类型;? extends E 则是一个上界通配符,可以用来指代 E 和 E 的任何子类。

个人理解,因为任何类型都可以胜任 ? 的场景,所以任何类型都可以看作是 ? 的子类;类似地, E 和 E 的任何子类也都可以看作是 ? extends E 的子类。

这篇博客中还强调,通配符 ? 指代的是类型实参而不是类型形参。个人理解,这是说 ? 是在使用处作为一个具体类型的指代,而不是在声明时作为一个待指定类型的标识符。从不能把 ? 当作一个名字使用的角度还是很好理解的。同样,<? extends E> 中的 E,它也是一个使用处指代类型实参的占位符,而不是一个等待指定的类型形参,所以官方示例里实际讨论时用的是 Collection<? extends Object> ,Object 就是 E 已经被指定的类型实参。

也因此,像 class GenericClass<?> 或者 class GenericClass<? extends E> 这样声明式的表述,都是不合法的。

  • 泛型为什么有对协变的需求

这个比较容易理解,就以 Collection<E> 为例。虽然 Collection<E> 作为一个泛型接口会提供抛出与接收这两类操作,因此必须是不型变的,但在具体使用时,一个 Collection<E> 的对象却有可能只进行抛出或接收操作。

就像示例中 addAll 函数的参数 items,它在方法中只会抛出元素,所以其实它抛出 E 或 E 的子类都可以胜任这个场景。也就是说,将类型实参是 E 的子类的泛型对象 items,视作是类型实参为 E 的泛型对象的子类,是合理的也是安全的。

因此,也就有了泛型对象延续类型实参的继承关系的需求,换句话说,根据类型实参的关系组织泛型对象的关系(回忆型变概念)。

  • <? extends E>的作用

依旧以 Collection<E> 为例。因为 <? extends E> 作为一个通配符类型参数,可以指代 E 和 E 的任意子类,所以当 A 是 E 的子类时,Collections<A> 便可以胜任 Collection<? extends E> 的场景,也就是说,Collections<A> 可以表现为 Collection<? extends E> 的子类。

因此可以这样认为:JAVA泛型便是利用上界通配符 <? extends E> 实现了泛型对类型实参之间的继承关系的传递的,也就是协变

除此之外,通配符在编译期间也保证了类型安全。例如,禁止向一个 ? extends E 类型的对象赋 E 类型的值,如此便可以限制一个协变的泛型对象的接收操作。如下有一个简单的代码示例。

public class Generics<T>{
    public T prop;
    public void coVariant(Generics<? extends T> x) {
        this.prop = x.prop;    // 通过  ? extends T 可以抛出给 T
        x.prop = this.prop;    // 报错  ? extends T 不可以接收 T
    }
}

 · JAVA泛型 PECS

...反过来,如果只能向集合中 放入 元素 , 就可以用 Object 集合并向其中放入 String:在JAVA中可以使用 List<? super String>,表示接受 String 和它的任意父类。

后者称为逆变性(contravariance),并且对于 List <? super String> 你只能调用接受 String 作为参数的方法...

Joshua Bloch 在其著作《Effective Java》第三版中很好地解释了该问题 (第 31 条:“利用有限制通配符来提升 API 的灵活性”)。 他称那些你只能从中读取的对象为生产者, 并称那些只能向其写入的对象为消费者。他建议: “为了灵活性最大化,在表示生产者或消费者的输入参数上使用通配符类型。”

他还提出了以下助记符:PECS 代表 生产者-Extends、消费者-Super(Producer-Extends, Consumer-Super)。

在了解了JAVA通过上界通配符实现的协变之后,那它的逆变就也很好理解了。

首先,逆变使用的通配符是下界通配符 <? super E>,它可以指代 E 和 E 的任何父类。

这意味着,当一个类 A 是 E 的父类时,那 Collection<A> 便会表现为 Collection<? super E> 的子类。即JAVA泛型利用通配符 <? super E> 实现泛型对类型实参之间的继承关系的逆转,也就是逆变

逆变的场景也比较容易想象,例如可以参照示例中的方法 addAll,假设有一个这样的方法,addTo(Collection<? super E> colls),它被用于将当前容器中的元素复制到另一个新容器 colls 中。如此,colls 便是一个仅做接收操作的 Collection<? super E> 对象。(其实没有这个方法,这里只是为了方便理解。


进一步地,JAVA 泛型将只进行抛出操作(也就是只从中读取)的对象称为生产者,只进行接收操作(也就是只向其写入)的对象则称为消费者,它们的类型参数可以安全地应用相应的 extends 或 super 通配符。

如此,也就理解了 JAVA 泛型中 Producer-Extends, Consumer-Super PECS 这一概念的含义了。


3、Kotlin 声明处 型变

· 使用处 型变 的局限

假设有一个泛型接口 Source<T>,该接口中不存在任何以 T 作为参数的方法,只是方法返回 T 类型值:

interface Source<T> {
    T nextT();
}

那么,在 Source <Object> 类型的变量中存储 Source <String> 实例的引用是极为安全的—— 没有消费者-方法可以调用。但是 Java 并不知道这一点,并且仍然禁止这样操作:

void demo(Source<String> strs) {
  Source<Object> objects = strs; // !!!在 Java 中不允许
   ……
}

为了修正这一点,我们必须声明对象的类型为 Source<? extends Object>。这么做毫无意义, 因为我们可以像以前一样在该对象上调用所有相同的方法,所以更复杂的类型并没有带来价值。 但编译器并不知道。

Kotlin首先介绍了JAVA泛型依靠通配符实现的型变的不足

个人理解,因为通配符是一种类型实参符号,不能用于声明一个泛型的类型形参,所以JAVA泛型的型变只能是一种使用处型变。这就导致那些只有抛出操作或只有接收操作的泛型接口或泛型类明明是型变安全的,但仍不得不在每个具体使用的场景中对泛型对象设置通配符类型参数。

针对这个不足,Kotlin泛型提出了它的声明处型变

· 声明处 协变 out

...在 Kotlin 中,有一种方法向编译器解释这种情况。这称为声明处型变: 可以标注 Source 的类型参数 T 来确保它仅从 Source<T> 成员中返回(生产),并从不被消费。 为此请使用 out 修饰符:

interface Source<out T> {
    fun nextT(): T
}
fun demo(strs: Source<String>) {
    val objects: Source<Any> = strs // 这个没问题,因为 T 是一个 out-参数
    // ……
}

一般原则是:当一个类 C 的类型参数 T 被声明为 out 时,它就只能出现在 C 的成员的输出位置, 但回报是 C<Base> 可以安全地作为 C<Derived> 的超类。

简而言之,可以说类 C 是在参数 T 上是协变的,或者说 T 是一个协变的类型参数。 可以认为 C 是 T 的生产者,而不是 T 的消费者

out 修饰符称为型变注解,并且由于它在类型参数声明处提供, 所以它提供了声明处型变

只要理解了类型实参与类型形参、使用处与声明处、以及协变的含义,这部分文档还是非常直观的。

Kotlin 修饰符关键字 out:

  • 场景:泛型类或泛型接口只会是类型参数的生产者,而不会是消费者;
  • 用法:在声明泛型类或泛型接口时,修饰类型形参
  • 作用:将类型参数标记为协变,即告知编译器,该泛型类或泛型接口可以安全地传递该类型实参间的继承关系,因此编译器会检查并限制对该类型参数的接收操作。

以如下代码为例,泛型类声明了 T 与 E 两个类型形参,使用 out 修饰 T 之后,T 类型的 var 属性就无法通过编译了(因为 var 属性有接收 T 类型的 Setter 访问器,即 T 的消费者)。

class GenericClass<out T, E> (initVT: T, initVE: E) {
    val propNoIn: T = initVT     // 编译通过
    var propCanIn: E = initVE    // 编译通过
    var propCantIn: T = initVT   // 编译不通过
    // 报错 Type parameter T is declared as 'out' but occurs in 'invariant' position in type T
}

 · 声明处 逆变 in

...Kotlin 又补充了一个型变注解:in。它使得一个类型参数逆变,即只可以消费而不可以生产。逆变类型的一个很好的例子是 Comparable:

interface Comparable<in T> {
    operator fun compareTo(other: T): Int
}
fun demo(x: Comparable<Number>) {
    x.compareTo(1.0) // 1.0 拥有类型 Double,它是 Number 的子类型
    // 因此,可以将 x 赋给类型为 Comparable <Double> 的变量
    val y: Comparable<Double> = x // OK!
}

Kotlin 修饰符关键字 in :

  • 场景:泛型类或泛型接口只会是类型参数的消费者
  • 作用:将类型参数标记为逆变,即告知编译器,该泛型类或泛型接口可以安全地逆转该类型实参间的继承关系。

可以从官方给出的 Comparable 示例理解消费者与逆变:Comparable 只有一个接收 T 类型对象的函数,所以 Comparable 是 T 的消费者;又因为 Comparable 使用 in 修饰类型参数 T,所以 Comparable 在 T 上是逆变的,即当 T 的类型实参 A 是 B 的子类时,Comparable<B> 是Comparable<A> 的子类,所以 Comparable<Number> 是 Comparable<Double> 的子类,所以 x 可以赋值为 y。

注意,示例中 x.compareTo(1.0) 这行代码和逆变没有关系,因为一个接收 Number 参数的函数本来就可以接收 Double ,因为 Double 是 Number 的子类。


· 补充:in 与 out 与 PECS

in 和 out 两词看起来是自解释的(因为它们已经在 C# 中成功使用很长时间了),因此上面提到的助记符不是真正需要的。可以将其改写为更高级的抽象:

存在性(The Existential)变换:消费者 in, 生产者 out!  :-)

虽然这里提到了C#,但其实“自解释self-explanatory”并不算什么特殊的编程概念,只是强调“不言自明”。

所以这段其实只是强调了 inout 这两个关键字相较于助记符 PECS优越性,因为 生产者-extends,消费者-super 明显是不如 生产者-out,消费者-in 意义明了的。

(至于存在性的变换,大概是说向存在主义的“存在先于本质”的转变吧,先存在再下定义什么的......已经不想再理解了......)

三、类型投影

简单总结上一章节,发现文档讲了这样一个事情:为了弥补JAVA只有使用处型变的缺陷,Kotlin提出了声明处型变;再简单回忆一下<二.2.3>这一小节中 JAVA 对使用处型变的需求的分析,就会发现Kotlin也同样有对使用处型变的需求(因为肯定有既生产又消费所以型变不安全的泛型类和接口)。

所以文档接着介绍了Kotlin自己的使用处型变方案,类型投影 Type Projection

 1、使用处型变:类型投影

 · 协变的类型安全问题

Kotlin文档中,以 Array<T> 的函数 fun copy(from: Array<Any>, to: Array<Any>) 为例,进行了一番与 <二.2.3> 中 的 Collection<E> 方法 void addAll(Collection<E> items) 的类似分析,并得出一个类似的问题:

... Array <T> 在 T 上是不型变的,因此 Array <Int> 与 Array <Any> 都不是另一个的子类型...

又将这种不型变的根本动机归结如下:

为什么? 再次重复,因为 copy 可能有非预期行为,例如它可能尝试写一个 String 到 from, 并且如果我们实际上传递一个 Int 的数组,以后会抛 ClassCastException 异常。

个人理解,这里强调的是类型安全方面的考虑,即如果允许协变的话,可能会存在危险的对“子类”对象的写操作。

回忆 JAVA 的使用处协变方案,它利用通配符 <? extends E> 实现泛型对象对类型实参间的继承关系的传递,同时,又通过禁止向一个 ? extends E 类型的对象赋 E 类型的值,限制了对“子类”对象的写操作。如下有一个简单的示例。(第二章中有给过一个类似的代码示例,这里稍作修改来帮助理解。)

public class Generics<T>{
    public T prop;
}
public void useSiteCov(Generics<? extends String> from, Generics<String> to){
    from.prop = to.prop;  // 编译报错
    // Required type: capture of ? extends String
    // Provided:      String
}

 · 类型投影 out 与 in

Kotlin又是怎么保证不写向“子类”的呢?

要禁止 copy 函数写向 from,你可以这样做:

fun copy(from: Array<out Any>, to: Array<Any>) { …… }

这就是类型投影:意味着 from 不仅仅是一个数组,而是一个受限制的(投影的)数组。 只可以调用返回类型为类型参数 T 的方法,如上,这意味着只能调用 get()。 这就是使用处型变的用法,并且是对应于 Java 的 Array<? extends Object>、 但更简单。


直观来看,Kotlin的使用处协变依旧是使用关键字 out,不过与Kotlin声明处协变不同的是,这次 out 修饰的是类型实参。至于它的作用,则是将泛型对象“投影”成一个只有返回类型为类型实参的方法的对象。

那该怎么理解“类型投影”这个概念呢?首先要考虑这个问题:

  • 投影是什么意思?

个人理解,这里可以参考数学中的向量投影概念:向量 b 在与它夹角为 θ 的向量 a 方向上的投影,为 |b|cosθ。更方便理解的一个例子,在一个二维坐标系中,向量 (1, 1) 在 X 轴方向上的投影为 1。

所以也可以这样认为——投影就是保留一个对象在某个方向上的信息,而丢弃其它方向上的信息。

从这个角度考虑,就可以把Kotlin泛型的类型投影理解为:

  • 类型在 生产/消费 方向上的投影

关键字 out 与 in 就分别对泛型对象进行 “生产” 与 “消费” 方向的投影。以 out 为例,它把泛型对象投影向生产的方向,就意味着它只保留了那些返回类型为该类型实参的方法,至于那些接收该类型实参的方法,便都被类型投影舍弃了。

如下有一个代码示例,可以发现编译器对那些被类型投影舍弃的方法进行了限制。

例如,编译器会在一个 out 投影对象的消费者方法中,将类型实参看作是 Nothing(在Kotlin中,Nothing 是所有类型的子类,而父类不可以赋值给子类),以此禁止它的接收操作,或者编译器会直接提示接收类型实参对象的 Setter 访问器已被类型投影移除。

而对于一个 in 投影对象,编译器会在它的生产者方法中,将它的类型实参看作是 Any?(在Kotlin中,Any? 是所有类型的父类),以限制它的抛出操作。

class GenericClass<T>(var prop: T, var propL: List<T>) {
}

fun typeProjection(outProj: GenericClass<out Int>, inProj: GenericClass<in Number>) {
    val outV = outProj.prop            // 通过,outV与prop类型为 Int
    val outVL = outProj.propL          // 通过,类型为 List<Int>
    outProj.prop = 1                   // 报错,提示 Setter for 'prop' is removed by type projection
    outProj.propL = outProj.propL      // 报错 Required: List<Nothing>   Found: List<Int>
    
    inProj.prop = 1                    // 通过,prop类型为Any?         
    inProj.prop = "123"                // 报错 Required: Number Found: String
    inProj.propL = listOf(1, 2, 3)     // 通过,propL类型为List<Any? >
    val inV: Number = inProj.prop      // 报错 Required: Number Found: Any?
    val inVL = inProj.propL            // 通过,但类型是List<Any? >
}

所以虽然乍一看会觉得“类型投影”这个名字很难懂,但其实它的内部逻辑依旧是 out-生产者,in-消费者。如此,作为类型实参的 <out T> 和 <? extends T>、<in T> 和 <? super T>,也就一一地对应了。

 2、星投影 和 泛型约束

Kotlin文档在下一部分才会介绍到泛型约束,但因为星投影包含泛型约束的内容,所以把后者移到星投影之前介绍

 · 泛型约束

能够替换给定类型参数的所有可能类型的集合可以由泛型约束限制。

最常见的约束类型是上界,与 Java 的 extends 关键字对应:

fun <T : Comparable<T>> sort(list: List<T>) { 
    …… 
}

冒号之后指定的类型是上界,表明只有 Comparable<T> 的子类型可以替代 T。例如:

sort(listOf(1, 2, 3))  //OK,Int 是 Comparable<Int> 的子类型
sort(listOf(HashMap<Int, String>())) // 错误,HashMap<Int, String> 不是 Comparable<HashMap<Int, String>> 的子类型

首先,泛型约束限制的是什么?个人理解,是限制可以给类型参数指定哪些类型实参,进一步地理解,就是说泛型约束是设置在声明时的类型形参上的。所以示例中,上界约束 : 设置在了泛型函数声明时的类型形参列表中。

至于上界约束的含义,就是限制类型实参的上界即父类,还是比较容易理解的。

Kotlin的 : 与JAVA的 extends 对应,因为JAVA泛型也可以在声明类型形参列表时设置上界约束。例如如下:

public class Generics<T extends Number>{
    ...
}

默认的上界(如果没有声明)是 Any?。在尖括号中只能指定一个上界。 如果同一类型参数需要多个上界,需要一个单独的 where-子句:

fun <T> copyWhenGreater(list: List<T>, threshold: T): List<String>
    where T : CharSequence,
          T : Comparable<T> {
    return list.filter { it > threshold }.map { it.toString() }
}

所传递的类型必须同时满足 where 子句的所有条件。在上述示例中,类型 T 必须 既 实现了 CharSequence 也 实现了 Comparable。


Kotlin泛型还提出了一个关键字,where,它可以在声明时尖括号 <> 外的一个单独的子句中设置多个约束条件,每个约束条件之间使用 , 隔开。

(JAVA也可以指定多个约束,但JAVA是在 <> 内用符号 & 连接多个上界,示例如下:)

public class Generics<T extends Number & Comparable<T>, E extends String> {
    ...
}

目前来看,泛型好像没有声明时的下界约束,例如JAVA泛型中的关键字 super 就不可以像 extends 那样用在声明处。

个人理解,这和JAVA与Kotlin泛型的原理有关(第四章有介绍):泛型实际上是将那些类型待指定的对象存储为了类型参数的父类,所以设置上界父类是非常自然又简单的事情,但设置下界子类实现起来就会很麻烦了。而且从抽象的角度考虑,指定类型实参,其实可以看作是一种赋值操作,子类可以安全地赋值给父类,父类却不可以赋值给子类,所以设置下界的意义并不大。

 · 星投影

有时你想说,你对类型参数(*原文为type argument,即类型实参*)一无所知,但仍然希望以安全的方式使用它。这里的安全方式是定义泛型类型的这种投影,该泛型类型的每个具体实例化都会是该投影的子类型。

Kotlin 为此提供了所谓的*星投影*语法:

  • 对于 Foo <out T : TUpper>,其中 T 是一个具有上界 TUpper 的协变类型参数,Foo <*> 等价于 Foo <out TUpper>。 意味着当 T 未知时,你可以安全地从 Foo <*> 读取 TUpper 的值。
  • 对于 Foo <in T>,其中 T 是一个逆变类型参数,Foo <*> 等价于 Foo <in Nothing>。 意味着当 T 未知时, 没有什么可以以安全的方式写入 Foo <*>。
  • 对于 Foo <T : TUpper>,其中 T 是一个具有上界 TUpper 的不型变类型参数,Foo<*> 对于读取值时等价于 Foo<out TUpper> 而对于写值时等价于 Foo<in Nothing>

如果从“投影就是保留一些方法、舍弃一些方法”的角度,来理解星投影,就会发现星投影是要保留那些所有泛型对象都可以安全地调用的方法,并基于这一事实,使星投影安全地表现为所有泛型对象的父类

此时查看Kotlin定义的星投影语法:

  • Foo <out T: TUpper> 的星投影 Foo <*> 等价于 Foo <out TUpper>

<out T: TUpper> 是一个声明处的类型形参列表,它表示:Foo 在类型参数 T 上是协变的,因此只有 T 的生产者方法,而没有 T 的消费者方法;T 的类型实参的共有父类是 TUpper。

所以,对于所有 Foo 的实例来说,它们共有的方法有且只有 TUpper 的生产者方法(子类的生产者也是父类的生产者,所以都是 TUpper 的生产者),所以,只需要将 Foo 向 TUpper 生产者的方向投影,就可以得到它的星投影,即 Foo<*> 等价于 Foo<out TUpper>

  • Foo <in T> 的星投影 <*> 等价于 <in Nothing>

<in T> 表示:Foo 在 T 上是逆变的,因此只有 T 的消费者方法,而没有 T 的生产者方法;T 的类型实参的共有子类是 Nothing。

所以,对于所有 Foo 的实例来说,它们共有的方法有且只有 Nothing 的消费者方法(父类的消费者也是子类的消费者,所以都是 Nothing 的消费者),所以,只需要将 Foo 向 Nothing 消费者的方向投影,就可以得到它的星投影,即 Foo<*> 等价于 Foo<in Nothing>

  •  Foo <T: TUpper> 的星投影 <*> 读时等价于 <out TUpper>,写时等价于 <in Nothing>

基于前面两个例子,这个例子就很好理解了:

Foo 在 T 上是不型变的,因此既有对 T 的生产者方法,又有对 T 的消费者方法;T 的类型实参的共有父类是 TUpper,共有子类是 Nothing。

所以,Foo 的所有实例共有的方法是 TUpper 的生产者 +  Nothing 的消费者,也就是在 out TUpper 方向上的投影和 in Nothing 方向上的投影的并集,表现为时等价于 <out TUpper>时等价于 <in Nothing>

如果泛型类型具有多个类型参数,则每个类型参数都可以单独投影。 例如,如果类型被声明为 interface Function <in T, out U>,可以使用以下星投影:

  • Function<*, String> 表示 Function<in Nothing, String>
  • Function<Int, *> 表示 Function<Int, out Any? >
  • Function<*, *> 表示 Function<in Nothing, out Any?>

    
简单总结,<*> 星投影就是只 out 生产类型实参们的共有父类,只 in 消费类型实参们的共有子类

而它这样做的最主要的目的,就是为了能安全地表现为所有泛型对象的父类,所以从作用上来看,星投影这个使用处的符号,和JAVA中的 ?、? extends T? super T 这些通配符是比较类似的。


 四、类型擦除

 1、什么是类型擦除?

Kotlin 为泛型声明用法执行的类型安全检测在编译期进行。 运行时泛型类型的实例不保留关于其类型实参的任何信息。 其类型信息称为被擦除。例如,Foo<Bar> 与 Foo<Baz? > 的实例都会被擦除为 Foo<*>。

Kotlin的类型擦除就是在编译时丢弃(也就是擦除)类型实参的相关信息,使得运行时无法获知这个泛型对象被指定了什么类型实参

查看一个泛型类 GenericClass<T, E: Number? > 编译之后再反编译的JAVA代码,可以发现 T 和 E 类型的对象,实质存储的类型是它们对应的上界类 Object 和 Number;而对于具体的泛型对象,无论它们被指定了什么类型实参,都是 GenericClass 对象(和 GenericClass<*> 等价,所以文档中说泛型 Foo 的实例会被擦除为 Foo<*>)。

class GenericClass<T, E: Number? >
val demo: GenericClass = GenericClass("1", 1)
val prop: String = demo.propT
public final class GenericClass {
   private Object propT;              // 所有可能的类型实参的父类:Object
   private Number propE;              // 所有可能的类型实参的父类:Number

   public GenericClass(Object propT, Number propE) {
      this.propT = propT;
      this.propE = propE;
   }

   public final Object getPropT() {   return this.propT;}
 
   public final void setPropT(Object var1) { this.propT = var1;}

   public final Number getPropE() { return this.propE; }

   public final void setPropE(Number var1) { this.propE = var1;}
}

GenericClass demo = new GenericClass("1", 1); // 无论指定了什么类型实参,类型都只是 GenericClass
String prop = (String)demo.getPropT();        // 编译自动添加了类型转换

个人理解,泛型之所以能实现在使用时再指定具体的类型实参,便是因为这些对象实质上的类型是它所有可能的类型实参的父类;而泛型的类型安全,则是依赖在编译期间进行类型检查与类型转换达成的。等到了运行时,由于类型实参已被擦除,类型检查与类型转换便都会遇到种种局限了。

所以也有这样一种说法,JAVA和Kotlin的泛型是一种伪泛型

 2、泛型类型检测与类型转换

由于类型擦除,并没有通用的方法在运行时检测一个泛型类型的实例是否通过指定类型实参所创建 ,并且编译器禁止这种 is 检测。当然,你可以对一个实例检测星投影的类型:

if (something is List<*>) {
    something.forEach { println(it) } // 每一项的类型都是 `Any?`
}

类似地,当已经让一个实例的类型参数(在编译期)静态检测, 就可以对涉及非泛型部分做 is 检测或者类型转换。请注意, 在这种情况下,会省略尖括号:

fun handleStrings(list: MutableList<String>) {
    if (list is ArrayList) {
        // list 智能转换为 ArrayList<String>
    }
}

省略类型参数的这种语法可用于不考虑类型参数的类型转换:list as ArrayList。

泛型函数调用的类型参数也同样只在编译期检测。在函数体内部, 类型参数不能用于类型检测,并且类型转换为类型参数(foo as T)也是非受检的。

接下来,便主要通过一些代码示例,来理解类型擦除给泛型的类型检测与类型转换带来的种种限制。

 · 类型检测

首先看一下泛型的类型检测,如下有一段以泛型类 GenericClass<T> 为例的代码:

open class GenericClass<T>(var prop: T) {
    fun checkTypeErase(demo: GenericClass<String>) {
        this.prop is String                      // 通过, T 类型的对象会保存自身的类型信息
        T is String                              // 编译报错,提示 Type parameter 'T' is not an expression 
        T::class                                 // 编译报错,提示 Cannot use 'T' as reified type parameter. Use a class instead. 
        "String" is T                            // 编译报错,提示 Cannot check for instance of erased type: T 

        this is GenericClass                     // 通过 
        this is GenericClass<*>                  // 通过 
        this is GenericClass<String>             // 编译报错,提示 Cannot check for instance of erased type: GenericClass<String> 

        demo is GenericClass<String>             // 通过,检查结果 is always true
        demo is GenericClass<T>                  // 编译报错,提示 Cannot check for instance of erased type: GenericClass<T>
        
        if (demo is ChildGenericClass){
            demo is ChildGenericClass<String>    // 通过,因为 String 的类型实参在这个函数体内有效 
        }
      
        (this as GenericClass<Int>).let{         // Unchecked cast: GenericClass<T> to GenericClass<Int>
            it is ChildGenericClass<Int>         // 通过,因为 Int 的类型实参在(this as GenericClass<Int>)的作用域内有效
        }
    }
}

class ChildGenericClass<T>(prop: T) : GenericClass<T>(prop)

可以发现:

  • T 类型的属性 prop 可以进行 is 具体类型 的类型检测,因为它保存了自身的类型数据,也就是“自己是什么”而不是“T是什么”;
  • 参数 demo 可以进行带类型实参 String 的类型检测,因为当前作用域也就是这个函数为它指定了一个在编译期内会被检测的带类型实参的类型 GenericClass<String>(但demo不能进行带其它类型实参的类型检测,例如 is GenericClass<Int> 就无法通过编译;而且 is GenericClass<String> 编译之后不会保留任何与 String 相关的信息,只会表现为一个 true 值);
  • (this as GenericClass<Int>) 可以进行带类型实参的类型检测,因为它通过 unchecked cast 转换为了一个带类型实参的类型,之后该实参便在它的作用域内有效(和 demo 一样有类似的限制)。当然,相比于参数 demo,这种不受检的类型转换会带来一些运行时的错误,接下来会在“类型转换”的小节中介绍。

除了这些情况外,任何类似于泛型对象的类型实参是什么(当前的 T 值是什么)、对象的类型是否是泛型对象的类型实参(对象的类是否是当前的 T 值)、泛型对象是否是某个具体的类型实参类的类型检测,都无法通过编译。

所以可以简单地总结,只要当前作用域内没有关于类型实参的直接信息,那么便不能进行任何关于类型实参的类型检查——因为类型实参被擦除了。

 · 类型转换

这部分以一个泛型函数的Kotlin代码和反编译后的JAVA代码为例:(类型转换的代码都可以通过编译,但会在运行时报错)

fun <E> castType(arg: GenericClass<E>, demo: GenericClass<String>) {
    val castAC = arg as ChildGenericClass     // 不考虑类型参数时,类型转换可以省略尖括号
    
    arg.prop = demo.prop as E                 // 提示Unchecked cast: String to E
    arg.prop = "123" as E                     // 提示 Unchecked cast: String to E
    demo.prop = arg.prop as String 
   
    val castAS = arg as GenericClass<String>  // 提示 Unchecked cast: GenericClass<E> to GenericClass<String>
    val castDE = demo as GenericClass<E>      // 提示 Unchecked cast: GenericClass<String> to GenericClass<E>

    // arg.prop = "123"                       // 编译报错,提示 Type mismatch
    if ((arg as GenericClass<String>) is GenericClass<String>) {    
        //提示: Unchecked cast: GenericClass<E> to GenericClass<String>
        // 提示:Check for instance is always 'true'
        // arg is GenericClass<String>         // 此处不再是类型实参的作用域,因此不允许带类型实参的类型检测
        arg.prop = "123"                      // 通过,提示 Smart cast to GenericClass<kotlin.String>
    }
}

val arg = ChildGenericClass<Int>(1)
castType(arg, GenericClass("1"))
val strV: String = arg.prop as String    // 提示 This cast can never succeed,但运行时通过
val intV: Int = arg.prop                 // 编译器无报错、无提示,但运行时报错 ClassCastException
public static final void castType(@NotNull GenericClass arg, @NotNull GenericClass demo) {
   Intrinsics.checkNotNullParameter(arg, "arg");
   Intrinsics.checkNotNullParameter(demo, "demo");
   
   ChildGenericClass castAC = (ChildGenericClass)arg;
   arg.setProp(demo.getProp());    // unchecked cast 
   arg.setProp((Object)"123");     // unchecked cast

   Object var3 = arg.getProp();
   Intrinsics.checkNotNull(var3, "null cannot be cast to non-null type kotlin.String");
   demo.setProp((String)var3);       
   GenericClass castAS = arg;      // unchecked cast 
   GenericClass castDE = demo;     // unchecked cast
   arg.setProp("123");             // unchecked cast + always true
}

ChildGenericClass arg = new ChildGenericClass(1);
castType((GenericClass)arg, new GenericClass("1"));
Object var4 = arg.getProp();
Intrinsics.checkNotNull(var4, "null cannot be cast to non-null type kotlin.String");
String strV = (String)var4;
int intV = ((Number)arg.getProp()).intValue();
  • as 类型转换

首先查看基于关键字 as 的强制类型转换,可以发现,只要是向带类型实参的类的强转,例如 as Eas GenericClass<E>as GenericClass<String> 等,便都会提示"uncheckd cast",也就是文档中提到的“非受检”的类型转换。

查看非受检的类型转换和 as String 的普通类型转换对应的反编译JAVA代码,就会发现,与普通的类型转换相比,unchecked cast 就像它的名字说的那样,既没有对类型对象的任何检查,也没有依据类型实参显式地指定强转向的类型(向 Object 的强转可以看作是没有强转),或者直接点说,对于强制类型转换中的类型实参,as 什么也没做

而这样不检查便直接赋值的类型转换,势必是会在运行时造成一些类型安全问题的。

  • 智能转换

Kotlin中最常见的一种智能转换场景,便是在 is 类型检测结果的作用域内,隐式地将对象转换为 is 判断通过的类型。以上面提到的一段代码为例:

...
if ((arg as GenericClass<String>) is GenericClass<String>) {
    ... 
}
...

这句代码先使用 as 将泛型对象 arg 强转为带类型实参 String 的泛型类型,接着又用 is 检测强转后的对象是否是带类型实参 String 的泛型类型,如此,检测结果始终都会是 true。之后,在这个 true 结果的作用域也就是大括号 {} 内,arg 的类型实参便会被隐式的智能转换为 String,这意味着:

  1. 泛型的编译期类型检查不会再禁止向 arg 中 E 类型的对象赋值一个 String 值;
  2. 这里不再是 显式类型实参 String 的作用域,因此不能再对 arg 进行带类型实参的类型检测。

而就像示例代码中展示的那样,类似的强制转换和智能转换,会破坏泛型在编译期的类型检查,进而造成运行时错误。而这个问题归根到底,还是类型擦除造成的。

 3、内联函数 + reified

... 唯一的一个例外,是带 reified 类型参数的内联函数,它们真实的类型实参会被内联到每个函数的调用点,因此可以对类型参数进行类型检测与类型转换。

inline fun <reified A, reified B> Pair<*, *>.asPairOf(): Pair<A, B>? {
    if (first !is A || second !is B) return null
    return first as A to second as B
}

val somePair: Pair<Any?, Any? > = "items" to listOf(1, 2, 3)

val stringToSomething = somePair.asPairOf<String, Any>()
val stringToInt = somePair.asPairOf<String, Int>()
val stringToList = somePair.asPairOf<String, List<*>>()
val stringToStringList = somePair.asPairOf<String, List<String>>() // Compiles but breaks type safety!

fun main() {
    println("stringToSomething = " + stringToSomething)
    println("stringToInt = " + stringToInt)
    println("stringToList = " + stringToList)
    println("stringToStringList = " + stringToStringList)
    //println(stringToStringList?.second?.forEach() {it.length}) // This will throw ClassCastException as list items are not String
}

 · reified 的作用与原理

reified,意思是“具体的”,作为关键字,它被用于在内联泛型函数的声明处修饰类型形参,将其标记为一个具体化的类型参数。官方文档则介绍它的作用是将内联函数的类型参数标记为在运行时可访问

  • 在理解 reified 的作用之前,先回忆一下内联函数 inline

内联函数与普通函数的本质不同,就是它会以内联的方式进行编译,又或者说,编译时内联函数会在调用点原地展开,又或者说,编译时内联函数的函数体会被复制并替换掉调用它的语句。至于内联函数的参数,它们会在这段复制的函数体中赋值给原本的形参,就像寻常的函数调用传参那样;而对于高阶内联函数,那些函数类型的参数则会在形参位置原地展开。

如下有一个泛型函数的反编译JAVA代码示例:

// 内联函数的定义
inline fun inlineFunc(strParam: String, inlineParam: () -> Unit) {
    print("这是内联函数的函数体")
    print("这是内联函数的普通参数: $strParam")
    inlineParam()
}
// 内联函数的调用语句
inlineFunc("这是内联函数的普通参数", {
    print("这是内联函数的函数类型参数的函数体")
})
//内联函数的调用语句 反编译之后对应的JAVA代码
String strParam$iv = "这是内联函数的普通参数";       // 形参赋值 参数命名+$iv
...
String var2 = "这是内联函数的函数体";
System.out.print(var2);
var2 = "这是内联函数的普通参数: " + strParam$iv;    // 普通参数 
System.out.print(var2);
...
String var4 = "这是内联函数的函数类型参数的函数体";   // 函数类型参数  就地展开
System.out.print(var4);
  • 接着,理解内联函数和 reified 是如何处理类型参数的。

先将文档示例代码中类型参数 B 的 reified 关键字去掉,查看 A 和 B 使用上的不同:

inline fun <reified A, B> Pair<*, *>.asPairOf(): Pair<A, B>? {
    A::class.objectInstance             // 编译通过
    //B::class                          // 报错 Cannot use 'B' as reified type parameter. Use a class instead. 
    
    if (first !is A) return null
    //if (second !is B)                 // 报错,Cannot check for instance of erased type: B

    return first as A to second as B    // as B 提示 unchecked cast
}

可以看到,内联函数中没有 reified 修饰的类型参数 B,就和普通的泛型类型参数一样,无法进行形如 is BB::class 等操作,as B 的强制类型转换也是 unchecked cast——当然,这也很好理解,因为这些就是具体类型才可以进行的操作,而内联函数本身并无法改变 B 只是一个编译后会被擦除的泛型类型参数的事实。

那么 reified 在这里起到的作用就很直观了,它将类型参数 A 标记为了一个具体类型,所以 A 可以不受限制地进行 is AA::class 以及受检的 as A 强转等操作。

至于 reified 做了哪些工作,可以先查看内联函数本身以及它的调用语句对应的反编译JAVA代码:

// 内联函数本身
public static final Pair asPairOf(Pair $this$asPairOf) {
   Intrinsics.checkNotNullParameter($this$asPairOf, "<this>");
   int $i$f$asPairOf = false;
   Intrinsics.reifiedOperationMarker(4, "A");     // A::class 对应 reified操作标记 4
   Reflection.getOrCreateKotlinClass(Object.class).getObjectInstance()  // 内联函数内,reified 类型参数仍是Object
   Object var10000 = $this$asPairOf.getFirst();
   Intrinsics.reifiedOperationMarker(3, "A");     // is A 对应 reified operation 标记 3 
   if (!(var10000 instanceof Object)) {           // 内联函数内,reified 类型参数仍是Object
      return null;
   } else {
      var10000 = $this$asPairOf.getFirst();
      Intrinsics.reifiedOperationMarker(1, "A");  // as A 对应 reified操作标记 1
      return TuplesKt.to((Object)var10000, (Object)$this$asPairOf.getSecond());  // 内联函数内,reified 类型参数仍是Object
   }
}

// 内联函数的调用语句
// val stringToStringList = somePair.asPairOf<String, List<String>>()
Pair $this$asPairOf$iv = somePair;                // 形参赋值 参数命名+ $iv
int $i$f$asPairOf = false;
Reflection.getOrCreateKotlinClass(String.class).getObjectInstance();
                                                  // 原函数体中的 A 替换为 String
Object var10000;
if (!($this$asPairOf$iv.getFirst() instanceof String)) {
                                                  // 原函数体中的 Object 替换为 String
   var10000 = null;
} else {
   var10000 = $this$asPairOf$iv.getFirst();
   if (var10000 == null) {                        // 有检查,因为 as A 不是 unchecked cast
      throw new NullPointerException("null cannot be cast to non-null type kotlin.String");  
   }

   TuplesKt.to((String)var10000, $this$asPairOf$iv.getSecond());    // 原函数体中的 Object 替换为 String
}

首先,可以看到,在内联函数本身的编译结果中,每个将 A 作为一个具体类型使用的地方都添加了一个具体操作标记 reifiedOperationMarker,并传入了操作 id 与类型形参标识符 "A"。个人理解,::class 对应操作 id 4,is 对应操作 id 3,as 对应操作 id 1,而且每次操作都要添加一次标记。(没找到reified操作id的相关资料

public static void reifiedOperationMarker(int id, String typeParameterIdentifier) 

同时可以看到,在具体操作标记之后、具体类型的位置上,内联函数本身的编译结果使用的是 Object,例如 Object.classinstanceof Object(Object)类型转换 等等。等到了内联函数的具体调用处,被复制的函数体代码中的 Object 便都被替代为了 A 对应的类型实参 String。

所以个人理解,reified 做的主要工作便是:

  1. 在每个具体类型操作之前添加包含操作 id 与类型形参标识符的标记;
  2. 在每个具体类型的位置使用 Object 类型占位;
  3. 等复制函数体到调用处时,根据每个具体操作标记,将 Object 更改为类型形参对应的类型实参,并根据操作 id 补全类型操作(例如向 String 的强转需要有类型检查)。

 · 依旧存在的类型擦除限制

不过,上面提到的限制依旧适用于类型检测与转换中的泛型类型的实例,例如,在类型检测 arg is T 中,如果 arg 是一个泛型类型实例,那么它自己的类型参数仍会被擦除。

这里“上面提到的限制”,应该就是指类型擦除带来的类型检测与类型转换上的限制。文档后面还给出了一段泛型实例 arg 类型擦除导致 unchecked cast 的代码示例:

inline fun <reified T> List<*>.asListOfType(): List<T>? =
    if (all { it is T })
        @Suppress("UNCHECKED_CAST") // 该注解可以禁止 unchecked cast的警告
        this as List<T> else        // 没有注解时,这里会提示:Unchecked cast: List<*> to List<T>
        null

而除了泛型实例对象的类型擦除外,个人理解,reified 的类型参数自身也会受到编译时类型擦除的影响,例如文档示例代码中,还给了这样一个会破坏类型安全的例子:

val stringToStringList = somePair.asPairOf<String, List<String>>()

它传入的 reified B 的类型实参是一个带类型实参的泛型类型 List<String>。根据上面对 reified 工作原理的介绍,可以知道,关于 B 的类型检测 is B 与类型转换 as B 会在编译时原地展开并替换为 instance of B类型实参 和 (B类型实参) 对应的字节码。可问题是,在编译阶段,还有 List<String> 这样一种类型吗?答案是没有,因为类型擦除,就是在编译时丢失类型实参。因此,reified B 对应的具体类型操作只能被替换成 instance of List 或者 (List) ,这也意味着,它依旧无法对带类型实参的泛型类型进行安全的检测与转换。

所以个人理解,内联函数 + reified 之所以可以成为泛型中“唯一的例外”,只是因为它会在编译阶段被复制到调用点并将类型参数替换为类型实参;但也是因为在编译阶段,已经不存在有带类型实参的泛型类型了,所以也就不存在替换为这类类型并进行类型检测与转换。所以,也可以这样认为:reified 突破的从来就不是类型擦除这个限制。类型擦除的事实无法改变。

 五、总结

因为泛型的内容又多又有些抽象,所以我也是边写边理思路,导致最后写得实在是有点太长了!也不知道中间有没有理解错误的地方,欢迎指正与探讨!(ง๑ •̀_•́)ง

最后简单总结一下各章的重点:

第一章:搞明白类型参数、形参实参、<>各自的含义;

第二、三章:分清型变和不型变、协变和逆变、生产和消费;分清泛型的声明处与使用处;分清JAVA与Kotlin各自在声明处与使用处采用了哪些泛型方案:

第四章:理解类型擦除;理解类型检测与类型转换的限制;理解reified的原理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值