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

时间:2017-08-29 23:11:14

标签: generics kotlin kotlin-reified-type-parameters

我试图了解reified关键字的用途,显然是it's allowing us to do reflection on generics

然而,当我把它放在外面时,效果一样好。任何人都在关心何时会产生实际的差异

3 个答案:

答案 0 :(得分:212)

TL; DR:什么是reified

有利
fun <T> myGenericFun(c: Class<T>) 

myGenericFun等通用函数的正文中,您无法访问T类型,因为它的仅在编译时可用erased在运行时。因此,如果要将泛型类型用作函数体中的普通类,则需要显式将类作为参数传递,如myGenericFun中所示。

如果使用具体化 inline创建T函数,即使在运行时也可以访问T的类型,因此您不需要另外传递Class<T>。您可以使用T,就像它是正常的类一样,例如您可能想要检查变量是否是 T实例,您可以轻松地执行此操作:myVar is T

这种inline reified类型T函数如下所示:

inline fun <reified T> myGenericFun()

reified如何运作

您只能将reifiedinline function结合使用。这样的函数使得编译器将函数的字节码复制到使用函数的每个地方(该函数正在&#34;内联&#34;)。 当您使用reified类型调用内联函数时,编译器知道用作类型参数的实际类型,并修改生成的字节码以直接使用相应的类。 因此,myVar is T之类的调用在字节码和运行时变为myVar is String(如果类型参数为String)。

实施例

让我们看一个显示reified有用的示例。 我们想要为名为String的{​​{1}}创建一个扩展函数,该函数尝试将JSON字符串转换为具有由函数的泛型toKotlinObject指定的类型的普通Kotlin对象。我们可以使用com.fasterxml.jackson.module.kotlin,第一种方法如下:

a)没有统一类型的第一种方法

T

fun <T> String.toKotlinObject(): T { val mapper = jacksonObjectMapper() //does not compile! return mapper.readValue(this, T::class.java) } 方法采用一种它应该解析readValue的类型。如果我们尝试获取类型参数JsonObject的{​​{1}},编译器会抱怨:&#34;无法使用&#39; T&#39;作为具体的类型参数。请改用班级。&#34;

b)使用明确的Class参数

进行解决方法
T

作为一种变通方法,可以将Class fun <T: Any> String.toKotlinObject(c: KClass<T>): T { val mapper = jacksonObjectMapper() return mapper.readValue(this, c.java) } 作为方法参数,然后将其用作Class的参数。这是有效的,并且是通用Java代码中的常见模式。它可以如下调用:

T

c)Kotlin方式:readValue

使用data class MyJsonType(val name: String) val json = """{"name":"example"}""" json.toKotlinObject(MyJsonType::class) 函数与reified类型参数inline,可以以不同方式实现此功能:

reified

除此之外,T无法inline fun <reified T: Any> String.toKotlinObject(): T { val mapper = jacksonObjectMapper() return mapper.readValue(this, T::class.java) } Class可以像使用普通类一样使用T。对于客户端,代码如下所示:

T

重要说明:使用Java

json.toKotlinObject<MyJsonType>() 类型的内联函数无法从Java 代码中调用。

答案 1 :(得分:7)

了解 reified 类型

泛型

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

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

这里我们隐式调用了 toString()value 函数,并且可以正常工作。

但是我们不能直接对 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(在 Kotlin 中)指定类型:

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 类型参数的主要目标是了解类型参数 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() 是一个标准库函数。如果您将该函数调用为 println(),则上面的 kotlin.collections.List<kotlin.String> 函数将打印 getGenericType<List<String>>()KType 包括 KClass、类型参数信息和可空性信息。知道 KType 后,您可以对其进行反思。


Java 互操作性

未声明 inline 类型参数的 reified 函数可以作为常规 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 类型的本质。

答案 2 :(得分:4)

简单

* reified是在编译时授予使用权限(在函数内访问T)

例如:

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

使用方式:

{{1}}