Kotlin Vocabulary | Reified: 类型擦除后再生计划

本文将会为你们介绍 Kotlin 的 "reified" 关键字,在介绍 "reified" 以前,咱们得先提一下泛型 (Generics)。泛型在编程领域中是一个很重要的概念,它提供了类型安全,并帮助开发者在编程时不须要进行显示的类型转换。泛型对编程语言的类型系统进行了扩展,从而容许一个类型或方法在保证编译时类型安全的前提下,还能够对不一样类型的对象进行操做。可是使用泛型也会有一些限制,好比当您在泛型函数中想要获取泛型所表示类型的具体信息时,编译器就会报错,提示说相关的信息不存在。而 "reified" 关键字,正是为了解决此类问题诞生的。java

没法获取泛型所表示的类型

没法获取泛型所表示的类型编程

这些类型信息丢失是由于 JVM 实现泛型的方式所致使的 (提示: 类型擦除,咱们会在以后讨论这个问题)。解决这一问题的一个方法,是将泛型实际表明的类型信息做为一个参数传递给函数。安全

fun <T> printType(classType: Class<T>) {
    print(classType::class.java)
}

这样的代码看起来也没有那么不可接受,可是在 Kotlin Vocabulary 系列的文章 中咱们就一直在强调,Kotlin 中尽可能不要出现样板代码,这样可让代码保持简洁。为了达到这一目标,Kotlin 提供了一个特别的关键字 reified,使用它就能够在泛型函数中获取所需的类型信息。只要您对泛型的实现方式有所了解,就可能会不由惊呼: 这怎么可能!下面就来看看这是如何在 Kotlin 中实现的。app

泛型

在 Java 5.0 版本以前并未支持泛型,那时 Java 中的 collection 是没有类型信息的。也就是说一个 ArrayList 并不会声明它内部所包含的数据类型究竟是 String、Integer 仍是别的类型。编程语言

List list = new ArrayList();
list.add("First String");
// 正常处理,没有错误
list.add(6);

在没有泛型支持时,任什么时候候想访问 collection 中的对象,都要作一次显式的类型转换。另外也没有相应的错误保障机制来防止出现非法的类型转换。函数

String str = (String)list.get(1); 
// 须要显示地进行转换和抛出异常

为了解决这个问题,Java 从 Java 5 开始支持泛型。有了这一特性支持,您能够将 collection 关联一个指定的类型,当您向 collection 中添加非指定类型的数据时,编译器就会发出警告。同时,您也不须要进行显式的类型转换了,这也会减小运行时异常的状况发生。性能

List<String> list = new ArrayList<>();
list.add("First String");
// 编译错误
list.add(6); 
// 无需进行类型转换
String str = list.get(0);

泛型是经过一种叫 类型擦除 (type erasure) 的技巧实现的。因为 Java 5 以前没有关联类型信息,编译器会先将全部类型替换为基本的 Object 类型,而后再进行必要的类型转换。经过将类型信息提供给编译器,类型擦除能够作到既保证编译时类型安全,又能够经过保持字节码同以前的 Java 版本相同来实现向后兼容。可是,当在泛型函数中须要获取类型信息时,类型擦除的实现方式就显得力不从心了。spa

Reified

Reified 关键字必须结合内联函数一块儿使用,它能让本该在编译阶段就被擦除的类型信息,可以在运行时被获取到。若是您还不熟悉内联函数,能够阅读《Kotlin Vocabulary | 内联函数的原理与应用》。code

简单地解释一下内联函数,若是一个函数被标记为 inline,那么 Kotlin 编译器会在全部使用该函数的地方将函数调用替换为函数体。这样作的好处是,编译器能够随意地在调用处对函数体进行修改,由于修改的函数体是被复制的,因此修改后不会影响到其他调用一样函数的地方。如果要在参数中使用 reified,那首先须要将函数标记为 inline,而后在泛型参数以前添加 reified 关键字便可。对象

inline fun <reified T> printType() {
  print(T::class.java)
}

fun printStringType(){
 // 用 String 类型调用被 reified 修饰的泛型函数
  printType<String>()  
}

让咱们反编译一下 Java 代码来探索其中的奥秘。从反编译后的代码中能够发现,当调用 reified 修饰的内联函数时,编译器会复制该函数体,并将泛型类型替换为实际使用的类型。这样,您就能够不用将类传递给函数也可以获取到相应类型信息了。

// 从字节码转换为 Java 的内联函数
   public static final void printType() {
      int $i$f$printType = 0;
      Intrinsics.reifiedOperationMarker(4, "T");
      Class var1 = Object.class;
      boolean var2 = false;
      System.out.print(var1);
   }
// 从字节码转换为 Java 代码的调用方
   public static final void printStringType() {
      int $i$f$printType = false;
      Class var1 = String.class;
      boolean var2 = false;
      System.out.print(var1);
   }

Reified 关键字只能同内联函数一块儿使用,所以内联函数使用的规则也一样适用于被 reified 修饰的函数。另外请牢记,Java 代码中不能访问被 reified 修饰的函数。Java 不支持内联,也就意味着在 Java 中的泛型参数不能逃脱被编译器擦除类型的命运。

Reified 一样还支持重载函数返回泛型类型,例如,如下函数能够返回 Int 或者 Float:

inline fun <reified T> calculate(value: Float): T {
   return when (T::class) {
       Float::class -> value as T
       Int::class -> value.toInt() as T
       else -> throw IllegalStateException("Only works with Float and Int")
   }
}
 
val intCall: Int = calculate(123643)
val floatCall: Float = calculate(123643)

通常来讲,具备相同输入参数和不一样返回类型的函数是不可以被重载的。使用内联函数,编译器能够在复制函数体时,一样将泛型返回类型替换为实际所表示的类型。若是您查看反编译后的 Java 代码,能够发现编译器在 intCall 变量中实际使用的是 Integer 类型,在 floatCall 变量中实际使用的是 Float 类型。

public final void call() {
      float value = 123643.0F;
      int $i$f$calculate = false;
      KClass var5 = Reflection.getOrCreateKotlinClass(Integer.class);
      Integer var10000;
      if (Intrinsics.areEqual(var5, Reflection.getOrCreateKotlinClass(Float.TYPE))) {
         var10000 = (Integer)value;
      } else {
         if (!Intrinsics.areEqual(var5, Reflection.getOrCreateKotlinClass(Integer.TYPE))) {
            throw (Throwable)(new IllegalStateException("Only works with Float and Int"));
         }

         var10000 = (int)value;
      }
      //这里用到了 Integer
      int intCall = ((Number)var10000).intValue(); 
      int $i$f$calculate = false;
      KClass var6 = Reflection.getOrCreateKotlinClass(Float.class);
      Float var8;
      if (Intrinsics.areEqual(var6, Reflection.getOrCreateKotlinClass(Float.TYPE))) {
         var8 = value;
      } else {
         if (!Intrinsics.areEqual(var6, Reflection.getOrCreateKotlinClass(Integer.TYPE))) {
            throw (Throwable)(new IllegalStateException("Only works with Float and Int"));
         }

         var8 = (Float)(int)value;
      }

      float floatCall = ((Number)var8).floatValue();  //这里用到了 Float
   }

Reified 容许您在使用泛型来进行编程的同时,还可以在运行时获取到泛型所表明的类型信息,这在以前是没法作到的。当您须要在内联函数中使用到类型信息,或者须要重载泛型返回值时,您可使用 reified。使用 reified 不会带来任何性能上的损失,可是若是被内联的函数过于复杂则,仍是可能会致使性能问题。由于 reified 必须使用内联函数,因此要保证内联函数的简短,而且遵循使用内联函数的最佳实践,以避免让性能受到损失。

相关文章
相关标签/搜索