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

Posted

技术标签:

【中文标题】Kotlin 中的 reified 关键字是如何工作的?【英文标题】:How does the reified keyword in Kotlin work? 【发布时间】:2018-02-07 12:41:26 【问题描述】:

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

但是,当我将其省略时,它也可以正常工作。任何人愿意解释这何时会产生实际的不同

【问题讨论】:

泛型类型参数在运行时被擦除,如果您还没有,请阅读类型擦除。内联函数上的具体类型参数不仅内联方法体,而且还有 泛型类型参数 允许您执行诸如 T::class.java 之类的事情(您无法使用普通泛型类型执行此操作) .发表评论是因为我现在没有时间充实完整的答案.. 它允许访问函数的具体泛型类型,而无需依赖反射,也无需将类型作为参数传递。 【参考方案1】:

TL;DR:reified 有什么用处

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

在像myGenericFun 这样的通用函数的主体中,您不能访问类型T,因为它仅在编译时可用,而erased 在运行时可用。因此,如果您想在函数体中将泛型类型用作普通类,您需要将类作为参数显式传递,如myGenericFun 所示。

如果您使用reified T 创建inline 函数,即使在运行时也可以访问T 的类型,因此您不需要传递Class&lt;T&gt;此外。您可以使用T,就好像它是一个普通的课程一样 - 例如。你可能想检查一个变量是否是T实例,你可以很容易地做到这一点:myVar is T

这样的inline 函数与reified 类型T 如下所示:

inline fun <reified T> myGenericFun()

reified 的工作原理

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


示例

让我们看一个例子来说明reified 的作用。 我们想要为String 创建一个名为toKotlinObject 的扩展函数,它尝试将JSON 字符串转换为具有函数的泛型类型T 指定的类型的普通Kotlin 对象。我们可以为此使用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,编译器会报错:“Cannot use 'T' as reified type parameter. Use a class instead.”

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)

TClass 不需要另外取,T 可以像普通类一样使用。对于客户端,代码如下所示:

json.toKotlinObject<MyJsonType>()

重要提示:使用 Java

reified 类型的内联函数不能从 Java 代码中调用

【讨论】:

感谢您的全面回复!这实际上是有道理的。我想知道的只是一件事,如果函数被内联,为什么需要 reified 呢?它会留下类型擦除并内联函数吗?这对我来说似乎有点浪费,如果你内联函数,你也可以内联正在使用的类型,或者我在这里看到了什么错误? 感谢您的反馈,实际上我忘了提一些可能会为您提供答案的内容:可以从 Java 调用普通的内联函数,但可以调用具有具体类型参数的内联函数不!我认为这就是为什么不是内联函数的每个类型参数都会自动具体化的原因。 如果函数混合了具体化和非具体化参数怎么办?这使得它无论如何都没有资格从 Java 调用,为什么不自动具体化所有类型参数呢?为什么 kotlin 需要为所有类型参数显式指定具体化? 如果堆栈中的上层调用者不需要 json.toKotlinObject(),而是 json.toKotlinObject() 用于不同的对象怎么办? 顺便说一句,我了解到ObjectMapper 的构造成本很高,因此在想要反序列化 one 对象时不应构造一个新对象。因此,有没有一种方法可以利用 reified 编写漂亮的代码并在反序列化过程中重用 objectmapper?谢谢【参考方案2】:

了解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")

编译器删除类型参数部分&lt;String&gt;,运行时剩下的就是:

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")

所以,上面显示的两个代码sn-ps是等价的。

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


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

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

假设我们有一份不同类型水果的清单:

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

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

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

没有reified

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

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

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

类型检查

在检查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类型的泛型

有时类型参数可以是泛型类型。例如,函数调用doSomething&lt;List&lt;String&gt;&gt;()中的List&lt;String&gt;。由于具体化,有可能知道整个类型:

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

这里的typeOf() 是一个标准库函数。如果您将函数调用为getGenericType&lt;List&lt;String&gt;&gt;(),则上面的println() 函数将打印kotlin.collections.List&lt;kotlin.String&gt;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 类型可以帮助我们摆脱以下错误和警告:

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

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

【讨论】:

这比接受的答案更明确,谢谢! 很好的解释。谢谢。但是为什么类型被擦除了? @VengateshMurugasamy,如果要保留泛型类型,它们会消耗 JVM 中的大量内存和其他资源。因此,JVM 设计人员做出了一个设计决策,即在运行时擦除泛型类型。它们在编译时对类型安全很有用。 不确定其他答案是否解释清楚。但我确定我在阅读上述解释后理解了具体类型。谢谢@YogeshUmeshVaity【参考方案3】:

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)

【讨论】:

以上是关于Kotlin 中的 reified 关键字是如何工作的?的主要内容,如果未能解决你的问题,请参考以下文章

Kotlin泛型 ③ ( 泛型 out 协变 | 泛型 in 逆变 | 泛型 invariant 不变 | 泛型逆变协变代码示例 | 使用 reified 关键字检查泛型参数类型 )

Kotlin泛型总结 ★ ( 泛型类 | 泛型参数 | 泛型函数 | 多泛型参数 | 泛型类型约束 | 可变参数结合泛型 | out 协变 | in 逆变 | reified 检查泛型参数类型 )

Kotlin:Reified类型参数使Gson失败

Kotlinx 序列化:如何绕过 reified typeargs 进行反序列化?

Kotlin | 浅谈 reified 与泛型 那些事

Kotlin 判断泛型类型 reified