了解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(在 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<Any>上写一个扩展函数,如下所示:
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()。此函数以类似的方式使用了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类型的泛型
有时类型参数可以是泛型类型。例如,函数调用doSomething<List<String>>()中的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类型的本质有所帮助。