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

hl3*_*kel 98 generics kotlin kotlin-reified-type-parameters

我试图了解reified关键字的目的,显然它允许我们对泛型进行反思.

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

s1m*_*nw1 271

TL; DR:有什么reified好处

fun <T> myGenericFun(c: Class<T>) 
Run Code Online (Sandbox Code Playgroud)

在泛型函数的主体中myGenericFun,您无法访问该类型,T因为它仅在编译时可用,但在运行时被擦除.因此,如果要将泛型类型用作函数体中的普通类,则需要将类显式作为参数传递,如下所示myGenericFun.

如果inline使用reified 创建函数T,则T即使在运行时也可以访问类型,因此您无需Class<T>另外传递.您可以T像使用普通类一样工作,例如,您可能想要检查变量是否是一个实例 T,您可以轻松地执行此操作:myVar is T.

这种类型的inline函数如下所示:reifiedT

inline fun <reified T> myGenericFun()
Run Code Online (Sandbox Code Playgroud)

怎么reified工作

您只能reifiedinline功能组合使用.这样的函数使得编译器将函数的字节码复制到每个使用函数的地方(函数被"内联").当您使用reified类型调用内联函数时,编译器会知道用作类型参数的实际类型,并修改生成的字节码以直接使用相应的类.因此呼吁像myVar is TmyVar 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)
}
Run Code Online (Sandbox Code Playgroud)

readValue方法采用一种它应该解析JsonObject为的类型.如果我们尝试获取Classtype参数T,编译器会抱怨:"不能使用'T'作为reified类型参数.请改用类."

b)使用显式Class参数进行解决方法

fun <T: Any> String.toKotlinObject(c: KClass<T>): T {
    val mapper = jacksonObjectMapper()
    return mapper.readValue(this, c.java)
}
Run Code Online (Sandbox Code Playgroud)

作为一种解决方法,可以将Classof of T设为方法参数,然后将其用作参数readValue.这是有效的,并且是通用Java代码中的常见模式.它可以如下调用:

data class MyJsonType(val name: String)

val json = """{"name":"example"}"""
json.toKotlinObject(MyJsonType::class)
Run Code Online (Sandbox Code Playgroud)

c)Kotlin方式: reified

使用inline带有reified类型参数T的函数可以以不同方式实现该函数:

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

有没有需要采取ClassT额外,T可作为如果它是一个普通类.对于客户端,代码如下所示:

json.toKotlinObject<MyJsonType>()
Run Code Online (Sandbox Code Playgroud)

重要说明:使用Java

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

  • 感谢您的反馈,实际上我忘记提及可能给您答案的内容:可以从*Java*调用普通的内联函数,但是具有reified类型参数的函数不能!我认为这就是为什么内联函数的每个类型参数都不会自动生成的原因. (5认同)
  • 感谢您的全面回复!这实际上是有道理的.我想知道的一件事是,如果功能被内联,为什么需要具体化?它会留下类型擦除并内联函数吗?这对我来说似乎是一种浪费,如果你内联函数你也可以内联使用的类型,或者我在这里看错了什么? (3认同)
  • 如果函数是具体化和非具体化参数的混合怎么办?这使得它无论如何都无法从 Java 调用,为什么不自动具体化所有类型参数呢?为什么 kotlin 需要显式地具体化所有类型参数? (2认同)
  • 如果堆栈中的上层调用者不需要 json.toKotlinObject&lt;MyJsonType&gt;(),而是针对不同对象的 json.toKotlinObject&lt;T&gt;() 怎么办? (2认同)

Yog*_*ity 13

理解reified类型

泛型

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

fun <T> doSomething(value: T) {
    println("Doing something with value: $value")                 // OK
}
Run Code Online (Sandbox Code Playgroud)

这里我们隐式调用toString()value和的函数。

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

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

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

类型擦除

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

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

doSomething<String>("Some String")
Run Code Online (Sandbox Code Playgroud)

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

doSomething("Some String")
Run Code Online (Sandbox Code Playgroud)

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

Java解决方案

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

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

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

类型具体化

上述问题的最佳解决方案是 Kotlin 中的类型具体化。所述reified类型参数之前改性剂能够在运行时被保持的类型的信息:

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

在上面的代码中,多亏了reifiedtype 参数,我们在对 type 执行操作时不再出现错误T。让我们看看inline函数如何使这种魔法成为可能。

inline 职能

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

fun main() {
    doSomething<String>("Some String")
}
Run Code Online (Sandbox Code Playgroud)

被编译为:

fun main() {
    println("Doing something with type: ${String::class.simpleName}")
}
Run Code Online (Sandbox Code Playgroud)

因此,上面显示的两个代码片段是等效的。

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


类型检查和类型的铸造reified类型

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

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

val fruits = listOf(Apple(), Orange(), Banana(), Orange())
Run Code Online (Sandbox Code Playgroud)

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

val oranges = listOf(Orange(), Orange())
Run Code Online (Sandbox Code Playgroud)

没有 reified

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

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

在此代码中,首先我们过滤类型,并且仅在元素的类型与给定的类型参数匹配时才采用元素。然后,我们投的每个元素给定的类型参数和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 }
}
Run Code Online (Sandbox Code Playgroud)

然后像下面这样调用它:

val oranges = fruits.filterFruit<Orange>()
Run Code Online (Sandbox Code Playgroud)

我展示了这个函数是为了更容易演示。为了过滤集合中的类型,已经有一个标准库函数filterIsInstance()。此函数以类似的方式使用了inlinereified修饰符。你可以简单地调用它如下:

val oranges = fruits.filterIsInstance<Orange>()
Run Code Online (Sandbox Code Playgroud)

reified参数作为参数传递

所述reified改性剂使得有可能为一个函数类型参数传递作为类型参数到具有另一功能reified调节剂:

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

inline fun <reified T> doSomethingElse() { }
Run Code Online (Sandbox Code Playgroud)

获取类型的泛型reified类型

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

inline fun <reified T> getGenericType() {
    val type: KType = typeOf<T>()
    println(type)
}
Run Code Online (Sandbox Code Playgroud)

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);
Run Code Online (Sandbox Code Playgroud)

你得到 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类型的本质。

  • @VengateshMurugasamy,如果保留泛型类型,它们会消耗 JVM 中的大量内存和其他资源。因此,JVM 设计者做出的设计决定是在运行时删除泛型类型。它们在编译时对于类型安全很有用。 (4认同)
  • 不确定其他答案的解释是否足够清楚。但我确信在阅读上述解释后我理解了具体化类型。谢谢@YogeshUmeshVaity (3认同)
  • 这比接受的答案更明确,谢谢! (2认同)
  • 很好的解释。谢谢。但为什么字体会被删除呢? (2认同)
  • 非常感谢您的精彩解释!这真的很清楚,也很切中要点;) (2认同)

小智 5

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

例如:

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

使用:

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