阅读 702

Kotlin的独门秘籍Reified实化类型参数(下篇)

Kotlin系列文章,欢迎查看:

原创系列:

翻译系列:

实战系列:

简述: 今天我们开始接着原创系列文章,首先说下为什么不把这篇作为翻译篇呢?我看了下作者的原文,里面讲到的,这篇博客都会有所涉及。这篇文章将会带你全部弄懂Kotlin泛型中的reified实化类型参数,包括它的基本使用、源码原理、以及使用场景。有了上篇文章的介绍,相信大家对kotlin的reified实化类型参数有了一定认识和了解。那么这篇文章将会更加完整地梳理Kotlin的reified实化类型参数的原理和使用。废话不多说,直接来看一波章节导图:

一、泛型类型擦除

通过上篇文章我们知道了JVM中的泛型一般是通过类型擦除实现的,也就是说泛型类实例的类型实参在编译时被擦除,在运行时是不会被保留的。基于这样实现的做法是有历史原因的,最大的原因之一是为了兼容JDK1.5之前的版本,当然泛型类型擦除也是有好处的,在运行时丢弃了一些类型实参的信息,对于内存占用也会减少很多。正因为泛型类型擦除原因在业界Java的泛型又称伪泛型。因为编译后所有泛型的类型实参类型都会被替换Object类型或者泛型类型形参指定上界约束类的类型。例如: List<Float>、List<String>、List<Student>在JVM运行时Float、String、Student都被替换成Object类型,如果是泛型定义是List<T extends Student>那么运行时T被替换成Student类型,具体可以通过反射Erasure类可看出。

虽然Kotlin没有和Java一样需要兼容旧版本的历史原因,但是由于Kotlin编译器编译后出来的class也是要运行在和Java相同的JVM上的,JVM的泛型一般都是通过泛型擦除,所以Kotlin始终还是迈不过泛型擦除的坎。但是Kotlin是一门有追求的语言不想再被C#那样喷Java说什么泛型集合连自己的类型实参都不知道,所以Kotlin借助inline内联函数玩了个小魔法。

二、泛型擦除会带来什么影响?

泛型擦除会带来什么影响,这里以Kotlin举例,因为Java遇到的问题,Kotlin同样需要面对。来看个例子

fun main(args: Array<String>) {
    val list1: List<Int> = listOf(1,2,3,4)
    val list2: List<String> = listOf("a","b","c","d")
    println(list1)
    println(list2)
}
复制代码

上面两个集合分别存储了Int类型的元素和String类型的元素,但是在编译后的class文件中的他们被替换成了List原生类型一起来看下反编译后的java代码

@Metadata(
   mv = {1, 1, 11},
   bv = {1, 0, 2},
   k = 2,
   d1 = {"\u0000\u0014\n\u0000\n\u0002\u0010\u0002\n\u0000\n\u0002\u0010\u0011\n\u0002\u0010\u000e\n\u0002\b\u0002\u001a\u0019\u0010\u0000\u001a\u00020\u00012\f\u0010\u0002\u001a\b\u0012\u0004\u0012\u00020\u00040\u0003¢\u0006\u0002\u0010\u0005¨\u0006\u0006"},
   d2 = {"main", "", "args", "", "", "([Ljava/lang/String;)V", "Lambda_main"}
)
public final class GenericKtKt {
   public static final void main(@NotNull String[] args) {
      Intrinsics.checkParameterIsNotNull(args, "args");
      List list1 = CollectionsKt.listOf(new Integer[]{1, 2, 3, 4});//List原生类型
      List list2 = CollectionsKt.listOf(new String[]{"a", "b", "c", "d"});//List原生类型
      System.out.println(list1);
      System.out.println(list2);
   }
}
复制代码

我们看到编译后listOf函数接收的是Object类型,不再是具体的String和Int类型了。

1、类型检查问题:

Kotlin中的is类型检查,一般情况不能检测类型实参中的类型(注意是一般情况,后面特殊情况会细讲),类似下面。

if(value is List<String>){...}//一般情况下这样的代码不会被编译通过
复制代码

分析: 尽管我们在运行时能够确定value是一个List集合,但是却无法获得该集合中存储的是哪种类型的数据元素,这就是因为泛型类的类型实参类型被擦除,被Object类型代替或上界形参约束类型代替。但是如何去正确检查value是否List呢?请看以下解决办法

Java中的解决办法: 针对上述的问题,Java有个很直接解决方式,那就是使用List原生类型。

if(value is List){...}
复制代码

Kotlin中的解决办法: 我们都知道Kotlin不支持类似Java的原生类型,所有的泛型类都需要显示指定类型实参的类型,对于上述问题,kotlin中可以借助星投影List<*>(关于星投影后续会详细讲解)来解决,目前你暂且认为它是拥有未知类型实参的泛型类型,它的作用类似Java中的List<?>通配符。

if(value is List<*>){...}
复制代码

特殊情况: 我们说is检查一般不能检测类型实参,但是有种特殊情况那就是Kotlin的编译器智能推导(不得不佩服Kotlin编译器的智能)

fun printNumberList(collection: Collection<String>) {
    if(collection is List<String>){...} //在这里这样写法是合法的。
}
复制代码

分析: Kotlin编译器能够根据当前作用域上下文智能推导出类型实参的类型,因为collection函数参数的泛型类的类型实参就是String,所以上述例子的类型实参只能是String,如果写成其他的类型还会报错呢。

2、类型转换问题:

在Kotlin中我们使用as或者as?来进行类型转换,注意在使用as转换时,仍然可以使用一般的泛型类型。只有该泛型类的基础类型是正确的即使是类型实参错误也能正常编译通过,但是会抛出一个警告。一起来看个例子

package com.mikyou.kotlin.generic


fun main(args: Array<String>) {
    printNumberList(listOf(1, 2, 3, 4, 5))//传入List<Int>类型的数据
}

fun printNumberList(collection: Collection<*>) {
    val numberList = collection as List<Int>//强转成List<Int>
    println(numberList)
}
复制代码

运行输出

package com.mikyou.kotlin.generic


fun main(args: Array<String>) {
    printNumberList(listOf("a", "b", "c", "d"))//传入List<String>类型的数据
}

fun printNumberList(collection: Collection<*>) {
    val numberList = collection as List<Int>
    //这里强转成List<Int>,并不会报错,输出正常,
    //但是需要注意不能默认把类型实参当做Int来操作,因为擦除无法确定当前类型实参,否则有可能出现运行时异常
    println(numberList)
}
复制代码

运行输出

如果我们把调用地方改成setOf(1,2,3,4,5)

fun main(args: Array<String>) {
    printNumberList(setOf(1, 2, 3, 4, 5))
}

fun printNumberList(collection: Collection<*>) {
    val numberList = collection as List<Int>
    println(numberList)
}
复制代码

运行输出

分析: 仔细想下,得到这样的结果也很正常,我们知道泛型的类型实参虽然在编译期被擦除,泛型类的基础类型不受其影响。虽然不知道List集合存储的具体元素类型,但是肯定能知道这是个List类型集合不是Set类型的集合,所以后者肯定会抛异常。至于前者因为在运行时无法确定类型实参,但是可以确定基础类型。所以只要基础类型匹配,而类型实参无法确定有可能匹配有可能不匹配,Kotlin编译采用抛出一个警告的处理。

注意: 不建议这样的写法容易存在安全隐患,由于编译器只给了个警告,并没有卡死后路。一旦后面默认把它当做强转的类型实参来操作,而调用方传入的是基础类型匹配而类型实参不匹配就会出问题。

package com.mikyou.kotlin.generic


fun main(args: Array<String>) {
    printNumberList(listOf("a", "b", "c", "d"))
}

fun printNumberList(collection: Collection<*>) {
    val numberList = collection as List<Int>
    println(numberList.sum())
}
复制代码

运行输出

三、什么是reified实化类型参数函数?

通过以上我们知道Kotlin和Java同样存在泛型类型擦除的问题,但是Kotlin作为一门现代编程语言,他知道Java擦除所带来的问题,所以开了一扇后门,就是通过inline函数保证使得泛型类的类型实参在运行时能够保留,这样的操作Kotlin中把它称为实化,对应需要使用reified关键字。

1、满足实化类型参数函数的必要条件

  • 必须是inline内联函数,使用inline关键字修饰
  • 泛型类定义泛型形参时必须使用reified关键字修饰

2、带实化类型参数的函数基本定义

inline fun <reified T> isInstanceOf(value: Any): Boolean = value is T 
复制代码

对于以上例子,我们可以说类型形参T是泛型函数isInstanceOf的实化类型参数。

3、关于inline函数补充一点

我们对inline函数应该不陌生,使用它最大一个好处就是函数调用的性能优化和提升,但是需要注意这里使用inline函数并不是因为性能的问题,而是另外一个好处它能是泛型函数类型实参进行实化,在运行时能拿到类型实参的信息。至于它是怎么实化的可以接着往下看

四、实化类型参数函数的背后原理以及反编译分析

我们知道类型实化参数实际上就是Kotlin变得的一个语法魔术,那么现在是时候揭开魔术神秘的面纱了。说实在的这个魔术能实现关键得益于内联函数,没有内联函数那么这个魔术就失效了。

1、原理描述

我们都知道内联函数的原理,编译器把实现内联函数的字节码动态插入到每次的调用点。那么实化的原理正是基于这个机制,每次调用带实化类型参数的函数时,编译器都知道此次调用中作为泛型类型实参的具体类型。所以编译器只要在每次调用时生成对应不同类型实参调用的字节码插入到调用点即可。 总之一句话很简单,就是带实化参数的函数每次调用都生成不同类型实参的字节码,动态插入到调用点。由于生成的字节码的类型实参引用了具体的类型,而不是类型参数所以不会存在擦除问题。

2、reified的例子

带实化类型参数的函数被广泛应用于Kotlin开发,特别是在一些Kotlin的官方库中,下面就用Anko库(简化Android的开发kotlin官方库)中一个精简版的startActivity函数

inline fun <reified T: Activity> Context.startActivity(vararg params: Pair<String, Any?>) =
        AnkoInternals.internalStartActivity(this, T::class.java, params)
复制代码

通过以上例子可看出定义了一个实化类型参数T,并且它有类型形参上界约束Activity,它可以直接将实化类型参数T当做普通类型使用

3、代码反编译分析

为了好反编译分析单独把库中的那个函数拷出来取了startActivityKt名字便于分析。

class SplashActivity : BizActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.biz_app_activity_welcome)
        startActivityKt<AccountActivity>()//只需这样就直接启动了AccountActivity了,指明了类型形参上界约束Activity
    }
}

inline fun <reified T : Activity> Context.startActivityKt(vararg params: Pair<String, Any?>) =
        AnkoInternals.internalStartActivity(this, T::class.java, params)
复制代码

编译后关键代码

//函数定义反编译
 private static final void startActivityKt(@NotNull Context $receiver, Pair... params) {
      Intrinsics.reifiedOperationMarker(4, "T");
      AnkoInternals.internalStartActivity($receiver, Activity.class, params);//注意点一: 由于泛型擦除的影响,编译后原来传入类型实参AccountActivity被它形参上界约束Activity替换了,所以这里证明了我们之前的分析。
   }
//函数调用点反编译
protected void onCreate(@Nullable Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      this.setContentView(2131361821);
      Pair[] params$iv = new Pair[0];
      AnkoInternals.internalStartActivity(this, AccountActivity.class, params$iv);
      //注意点二: 可以看到这里函数调用并不是简单函数调用,而是根据此次调用明确的类型实参AccountActivity.class替换定义处的Activity.class,然后生成新的字节码插入到调用点。
}
复制代码

让我们稍微在函数加点输出就会更加清晰

class SplashActivity : BizActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.biz_app_activity_welcome)
        startActivityKt<AccountActivity>()
    }
}

inline fun <reified T : Activity> Context.startActivityKt(vararg params: Pair<String, Any?>) {
    println("call before")
    AnkoInternals.internalStartActivity(this, T::class.java, params)
    println("call after")
}
复制代码

反编译后

private static final void startActivityKt(@NotNull Context $receiver, Pair... params) {
      String var3 = "call before";
      System.out.println(var3);
      Intrinsics.reifiedOperationMarker(4, "T");
      AnkoInternals.internalStartActivity($receiver, Activity.class, params);
      var3 = "call after";
      System.out.println(var3);
   }

   protected void onCreate(@Nullable Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      this.setContentView(2131361821);
      Pair[] params$iv = new Pair[0];
      String var4 = "call before";
      System.out.println(var4);
      AnkoInternals.internalStartActivity(this, AccountActivity.class, params$iv);//替换成确切的类型实参AccountActivity.class
      var4 = "call after";
      System.out.println(var4);
   }
   
复制代码

五、实化类型参数函数的使用限制

这里说的使用限制主要有两点:

1、Java调用Kotlin中的实化类型参数函数限制

明确回答Kotlin中的实化类型参数函数不能在Java中的调用,我们可以简单的分析下,首先Kotlin的实化类型参数函数主要得益于inline函数的内联功能,但是Java可以调用普通的内联函数但是失去了内联功能,失去内联功能也就意味实化操作也就化为泡影。故重申一次Kotlin中的实化类型参数函数不能在Java中的调用

2、Kotlin实化类型参数函数的使用限制

  • 不能使用非实化类型形参作为类型实参调用带实化类型参数的函数
  • 不能使用实化类型参数创建该类型参数的实例对象
  • 不能调用实化类型参数的伴生对象方法
  • reified关键字只能标记实化类型参数的内联函数,不能作用与类和属性。

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

关注下面的标签,发现更多相似文章
评论