Kotlin 中的 reified 关键字是如何工作的?

How does the reified keyword in Kotlin work?

我正在尝试理解 reified 关键字的用途,显然 it's allowing us to do reflection on generics

但是,当我将其省略时,它仍然可以正常工作。有人愿意解释这何时会产生实际的 差异?

TL;DR:reified

有什么好处
fun <T> myGenericFun(c: Class<T>) 

在像 myGenericFun 这样的通用函数的主体中,您无法访问类型 T 因为它 仅在编译时可用 但是 erased 在运行时。因此,如果要在函数体中将泛型类型用作普通 class,则需要 显式传递 class 作为参数 ,如 myGenericFun.

如果你用reifiedT创建一个inline函数,即使在运行时也可以访问T的类型,并且因此您不需要另外传递 Class<T> 。您可以像使用普通 class 一样使用 T - 例如您可能想检查一个变量是否是 T 实例,然后您可以轻松地做到这一点:myVar is T.

reified 类型 Tinline 函数如下所示:

inline fun <reified T> myGenericFun()

reified 的工作原理

您只能将 reifiedinline 函数结合使用。通过这样做,您指示编译器将函数的字节码复制到调用该函数的每个位置(编译器“内联”该函数)。当您使用 reified 类型调用 inline 函数时,编译器必须能够知道作为类型参数传递的实际类型,以便它可以修改生成的字节码以使用相应的 class 直接地。因此,像 myVar is T 这样的调用在字节码中变成了 myVar is String(如果类型参数是 String)。


例子

让我们看一个例子,看看 reified 有多大用处。 我们想为 String 创建一个名为 toKotlinObject 的扩展函数,它试图将 JSON 字符串转换为普通 Kotlin 对象,其类型由函数的泛型类型 T 指定。我们可以为此使用 com.fasterxml.jackson.module.kotlin,第一种方法如下:

a) 没有具体化类型的第一种方法

fun <T> String.toKotlinObject(): T {
      val mapper = jacksonObjectMapper()
                                                        //does not compile!
      return mapper.readValue(this, T::class.java)
}

readValue 方法采用一种类型,它应该将 JsonObject 解析为该类型。如果我们尝试获取类型参数 TClass,编译器会抱怨:“无法使用 'T' 作为具体化的类型参数。使用 class相反。"

b) 使用显式 Class 参数

的解决方法
fun <T: Any> String.toKotlinObject(c: KClass<T>): T {
    val mapper = jacksonObjectMapper()
    return mapper.readValue(this, c.java)
}

作为变通方法,TClass 可以作为方法参数,然后用作 readValue 的参数。这有效并且是通用 Java 代码中的常见模式。可以这样调用:

data class MyJsonType(val name: String)

val json = """{"name":"example"}"""
json.toKotlinObject(MyJsonType::class)

c) Kotlin 方式:reified

使用带有 reified 类型参数 Tinline 函数可以实现不同的函数:

inline fun <reified T: Any> String.toKotlinObject(): T {
    val mapper = jacksonObjectMapper()
    return mapper.readValue(this, T::class.java)
}

不需要再取TClassT可以当普通的class使用。对于客户端,代码如下所示:

json.toKotlinObject<MyJsonType>()

重要提示:使用 Java

类型为 reified 的内联函数 无法从 Java 代码中调用。

reified的目的是允许函数在编译时使用T(在函数内访问它)。

例如:

inline fun <reified T:Any>  String.convertToObject(): T{
    val gson = Gson()
    return gson.fromJson(this,T::class.java)
}

使用:

val jsonStringResponse = "{"name":"bruno" , "age":"14" , "world":"mars"}"
val userObject = jsonStringResponse.convertToObject<User>()
println(userObject.name)

了解 reified 类型

泛型

在 Kotlin 中使用泛型时,我们可以对任何类型的值执行操作 T:

fun <T> doSomething(value: T) {
    println("Doing something with value: $value")                 // OK
}

这里我们隐式地调用了 valuetoString() 函数,并且有效。

但是我们不能直接对类型T进行任何操作:

fun <T> doSomething(value: T) {
    println("Doing something with type: ${T::class.simpleName}")  // Error
}

让我们了解这个错误的原因。

类型擦除

在上面的代码中,编译器给出了一个错误:Cannot use 'T' as reified type parameter. Use a class instead.这是因为在编译时,编译器从函数调用中删除了类型参数。

例如,如果您将函数调用为:

doSomething<String>("Some String")

编译器删除了类型参数部分<String>,在运行时剩下的就是:

doSomething("Some String")

这叫做类型擦除。因此,在运行时(在函数定义内),我们不可能确切地知道 T 代表哪种类型。

Java解决方案

Java 中这种类型擦除问题的解决方案是传递一个额外的参数,用 Class(在 Java 中)或 KClass(在科特林):

fun <T: Any> doSomething(value: T, type: KClass<T>) {
    println("Doing something with type: ${type.simpleName}")       // OK
}

这样我们的代码就不会受到类型擦除的影响。但是这个解决方案很冗长而且不是很优雅,因为我们必须声明它并用一个额外的参数调用它。此外,指定类型绑定 Any 是强制性的。

类型具体化

解决上述问题的最佳方案是在 Kotlin 中进行类型具体化。类型参数前的reified修饰符可以让类型信息在运行时保留:

inline fun <reified T> doSomething(value: T) {
    println("Doing something with type: ${T::class.simpleName}")    // OK
}

在上面的代码中,由于 reified 类型参数,我们在对类型 T 执行操作时不再出现错误。让我们看看 inline 函数如何让这个魔法成为可能。

inline 函数

当我们将函数标记为 inline 时,编译器会在调用该函数的任何地方复制该 inline 函数的实际主体。由于我们将 doSomething() 函数标记为 inline,因此以下代码:

fun main() {
    doSomething<String>("Some String")
}

编译为:

fun main() {
    println("Doing something with type: ${String::class.simpleName}")
}

所以,上面显示的两个代码片段是等价的。

在复制 inline 函数的主体时,编译器还将类型参数 T 替换为在函数调用中指定或推断的实际类型参数。例如,请注意类型参数 T 如何替换为实际类型参数 String.


reified 类型的类型检查和类型转换

reified 类型参数的主要 objective 是知道类型参数 T 在运行时表示的确切类型。

假设我们有一个不同类型水果的列表:

val fruits = listOf(Apple(), Orange(), Banana(), Orange())

我们想在单独的列表中过滤所有 Orange 类型,如下所示:

val oranges = listOf(Orange(), Orange())

没有reified

为了过滤水果类型,我们可以在List<Any>上写一个扩展函数,如下所示:

fun <T> List<Any>.filterFruit(): List<T> {
    return this.filter { it is T }.map { it as T }          // Error and Warning
}

在这段代码中,首先我们过滤类型,只有当元素的类型与给定的类型参数匹配时才获取元素。然后我们将每个元素转换为给定的类型参数和 return List。但是有两个问题。

类型检查

在类型检查 it is T 时,编译器向我们介绍了另一个错误:Cannot check for instance of erased type: T。这是您可能因类型擦除而遇到的另一种错误。

类型转换

在类型转换 it as T 时,我们也会收到警告:Unchecked cast: Any to T。由于类型擦除,编译器无法确认类型。

reified 类型来拯救

我们可以通过将函数标记为 inline 并将类型参数设置为 reified 来轻松克服这两个问题,如前所述:

inline fun <reified T> List<Any>.filterFruit(): List<T> {
    return this.filter { it is T }.map { it as T }
}

然后像下面这样调用它:

val oranges = fruits.filterFruit<Orange>()

为了方便演示,我展示了这个功能。为了过滤集合中的类型,已经有一个标准库函数filterIsInstance()。该函数以类似的方式使用了 inlinereified 修饰符。您可以简单地这样称呼它:

val oranges = fruits.filterIsInstance<Orange>()

reified 参数作为参数传递

reified 修饰符使函数可以将类型参数作为类型参数传递给另一个具有 reified 修饰符的函数:

inline fun <reified T> doSomething() {
    // Passing T as an argument to another function
    doSomethingElse<T>()
}

inline fun <reified T> doSomethingElse() { }

正在获取 reified 类型的泛型类型

有时类型参数可以是泛型类型。例如,函数调用中的 List<String> doSomething<List<String>>()。由于具体化,可以了解整个类型:

inline fun <reified T> getGenericType() {
    val type: KType = typeOf<T>()
    println(type)
}

这里的typeOf()是一个标准库函数。如果您将函数调用为 getGenericType<List<String>>(),则上面的 println() 函数将打印 kotlin.collections.List<kotlin.String>KType 包括 KClass、类型参数信息和可空性信息。一旦你知道 KType,你就可以对其进行反思。


Java 互操作性

不带 reified 类型参数声明的 inline 函数可以像常规 Java 函数一样从 Java 调用。但是用 reified 类型参数声明的那些不能从 Java.

调用

即使您使用反射调用它,如下所示:

Method method = YourFilenameKt.class.getDeclaredMethod("doSomething", Object.class);
method.invoke("hello", Object.class);

你得到了UnsupportedOperationException: This function has a reified type parameter and thus can only be inlined at compilation time, not called directly.


结论

在许多情况下,reified 类型可以帮助我们摆脱以下错误和警告:

  1. Error: Cannot use 'T' as reified type parameter. Use a class instead.
  2. Error: Cannot check for instance of erased type: T
  3. Warning: Unchecked cast: SomeType to T

就是这样!希望有助于理解 reified 类型的本质。