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
类型 T
的 inline
函数如下所示:
inline fun <reified T> myGenericFun()
reified
的工作原理
您只能将 reified
与 inline
函数结合使用。通过这样做,您指示编译器将函数的字节码复制到调用该函数的每个位置(编译器“内联”该函数)。当您使用 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
解析为该类型。如果我们尝试获取类型参数 T
的 Class
,编译器会抱怨:“无法使用 'T' 作为具体化的类型参数。使用 class相反。"
b) 使用显式 Class
参数
的解决方法
fun <T: Any> String.toKotlinObject(c: KClass<T>): T {
val mapper = jacksonObjectMapper()
return mapper.readValue(this, c.java)
}
作为变通方法,T
的 Class
可以作为方法参数,然后用作 readValue
的参数。这有效并且是通用 Java 代码中的常见模式。可以这样调用:
data class MyJsonType(val name: String)
val json = """{"name":"example"}"""
json.toKotlinObject(MyJsonType::class)
c) Kotlin 方式:reified
使用带有 reified
类型参数 T
的 inline
函数可以实现不同的函数:
inline fun <reified T: Any> String.toKotlinObject(): T {
val mapper = jacksonObjectMapper()
return mapper.readValue(this, T::class.java)
}
不需要再取T
的Class
,T
可以当普通的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
}
这里我们隐式地调用了 value
的 toString()
函数,并且有效。
但是我们不能直接对类型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()
。该函数以类似的方式使用了 inline
和 reified
修饰符。您可以简单地这样称呼它:
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
类型可以帮助我们摆脱以下错误和警告:
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
类型的本质。
我正在尝试理解 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
类型 T
的 inline
函数如下所示:
inline fun <reified T> myGenericFun()
reified
的工作原理
您只能将 reified
与 inline
函数结合使用。通过这样做,您指示编译器将函数的字节码复制到调用该函数的每个位置(编译器“内联”该函数)。当您使用 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
解析为该类型。如果我们尝试获取类型参数 T
的 Class
,编译器会抱怨:“无法使用 'T' 作为具体化的类型参数。使用 class相反。"
b) 使用显式 Class
参数
fun <T: Any> String.toKotlinObject(c: KClass<T>): T {
val mapper = jacksonObjectMapper()
return mapper.readValue(this, c.java)
}
作为变通方法,T
的 Class
可以作为方法参数,然后用作 readValue
的参数。这有效并且是通用 Java 代码中的常见模式。可以这样调用:
data class MyJsonType(val name: String)
val json = """{"name":"example"}"""
json.toKotlinObject(MyJsonType::class)
c) Kotlin 方式:reified
使用带有 reified
类型参数 T
的 inline
函数可以实现不同的函数:
inline fun <reified T: Any> String.toKotlinObject(): T {
val mapper = jacksonObjectMapper()
return mapper.readValue(this, T::class.java)
}
不需要再取T
的Class
,T
可以当普通的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
}
这里我们隐式地调用了 value
的 toString()
函数,并且有效。
但是我们不能直接对类型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()
。该函数以类似的方式使用了 inline
和 reified
修饰符。您可以简单地这样称呼它:
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
类型可以帮助我们摆脱以下错误和警告:
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
类型的本质。