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

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

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

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

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

这些类型信息丢失是因为 JVM 实现泛型的方式所导致的 (提示: 类型擦除,我们会在之后讨论这个问题)。解决这一问题的一个方法,是将泛型实际代表的类型信息作为一个参数传递给函数。

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

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

泛型

在 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 版本相同来实现向后兼容。但是,当在泛型函数中需要获取类型信息时,类型擦除的实现方式就显得力不从心了。

Reified

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

简单地解释一下内联函数,如果一个函数被标记为 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 必须使用内联函数,所以要保证内联函数的简短,并且遵循使用内联函数的最佳实践,以免让性能受到损失。

版权声明

禁止一切形式的转载-禁止商用-禁止衍生 申请授权

脉脉不得语
脉脉不得语
Zhengzhou Website
Android Developer | https://androiddevtools.cn and https://androidweekly.io Funder | GDG Zhengzhou Funder & Ex Organizer | http://Toast.show(∞) Podcast Host

你已经成功订阅到 Android 开发技术周报
太棒了!接下来,完成检验以获得全部访问权限 Android 开发技术周报
欢迎回来!你已经成功登录了。
Unable to sign you in. Please try again.
成功!您的帐户已完全激活,您现在可以访问所有内容。
Error! Stripe checkout failed.
Success! Your billing info is updated.
Error! Billing info update failed.
🍗