在Kotlin中具体化关键字是如何工作的?

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

然而,当我把它去掉时,它也能正常工作。有人愿意解释一下什么时候这是一个真正的区别吗?

90578 次浏览

reified有什么好处

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

在像myGenericFun这样的泛型函数体中,你不能访问类型T,因为它在运行时是仅在编译时可用而不是<强>删除< / >强。因此,如果你想在函数体中使用泛型类型作为普通类,你需要显式地将类作为参数传递,如myGenericFun所示。

如果使用具体化 T创建inline函数,则即使在运行时也可以访问T类型,因此不需要额外传递Class<T>。你可以像使用普通类一样使用T——例如,你可能想检查一个变量是否为的实例 T,这很容易做到:myVar is T

这样一个具有reified类型Tinline函数如下所示:

inline fun <reified T> myGenericFun()

reified是如何工作的

只能将reifiedinline函数结合使用。通过这样做,你指示编译器将函数的字节码复制到函数调用的每个位置(编译器的“内联”;函数)。当调用具有reified类型的inline函数时,编译器必须能够知道作为类型参数传递的实际类型,以便它可以修改生成的字节码以直接使用相应的类。因此,像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”作为具体化的类型参数。用课堂代替。

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可以像普通类一样使用。对于客户端,代码是这样的:

json.toKotlinObject<MyJsonType>()

重要提示:使用Java

具有reified类型的内联函数是不可从Java调用 code。

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

有时类型参数可以是泛型类型。例如,函数调用doSomething<List<String>>()中的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的互操作性

声明的没有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类型的本质。