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

然而,当我把它去掉的时候,它仍然可以正常工作。这在什么时候会产生实际的影响?


当前回答

具体化的目的是允许函数在编译时使用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)

其他回答

具体化的好处是什么

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

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

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

这样一个具体化类型T的内联函数如下所示:

inline fun <reified T> myGenericFun()

物化是如何工作的

You can only use reified in combination with an inline function. By doing so, you instruct the compiler to copy the function's bytecode to every spot the function is invoked from (the compiler "inlines" the function). When you call an inline function with reified type, the compiler has to be able to know the actual type passed as a type argument so that it can modify the generated bytecode to use the corresponding class directly. Therefore a call like myVar is T becomes myVar is String in the bytecode (if the type argument is String).


例子

让我们看一个例子,它展示了物化是多么有用。 我们想为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'作为具体化的类型形参。还是上课吧。”

b)使用显式Class参数的解决方案

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

作为一种变通方法,可以将T的类作为方法参数,然后将其用作readValue的参数。这是通用Java代码中的一种常见模式。可以这样调用:

data class MyJsonType(val name: String)

val json = """{"name":"example"}"""
json.toKotlinObject(MyJsonType::class)

c) Kotlin方式:具象化

使用带具体化类型参数T的内联函数可以以不同的方式实现函数:

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

没有必要额外学习T的类,T可以作为一个普通的类使用。对于客户端,代码是这样的:

json.toKotlinObject<MyJsonType>()

重要提示:使用Java

具有具体化类型的内联函数不能从Java代码中调用。

具体化的目的是允许函数在编译时使用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中使用泛型时,我们可以对任何类型T的值执行操作:

fun <T> doSomething(value: T) {
    println("Doing something with value: $value")                 // OK
}

这里我们隐式调用值的toString()函数,这是可行的。

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

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

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

类型擦除

在上面的代码中,编译器给出了一个错误:不能使用'T'作为具体化的类型参数。使用类代替。这是因为在编译时,编译器从函数调用中删除了类型参数。

例如,如果你调用函数为:

doSomething<String>("Some String")

编译器删除类型参数part <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中的类型具体化。类型参数前的具体化修饰符使类型信息在运行时被保留:

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

在上面的代码中,由于具体化的类型参数,我们在对类型t执行操作时不再得到错误。让我们看看内联函数是如何实现这一魔力的。

内联函数

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

fun main() {
    doSomething<String>("Some String")
}

编译为:

fun main() {
    println("Doing something with type: ${String::class.simpleName}")
}

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

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


具体化类型的类型检查和类型铸造

具象化类型参数的主要目标是了解类型参数T在运行时表示的确切类型。

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

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

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

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

没有具体化

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

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

在这段代码中,首先对类型进行筛选,只有当元素的类型与给定的类型参数匹配时才取元素。然后将每个元素强制转换为给定的类型参数并返回List。但有两个问题。

类型检查

当类型检查它是T时,我们被编译器引入了另一个错误:不能检查已擦除类型的实例:T。这是由于类型擦除而可能遇到的另一种错误。

型铸造

当将其类型转换为T时,还会给出一个警告:未检查的类型转换:任意转换为T。由于类型擦除,编译器无法确认类型。

具象化的类型来拯救

我们可以通过将函数标记为内联并将类型参数具体化来轻松克服这两个问题:

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()。此函数以类似的方式使用内联和具体化修饰符。你可以像这样简单地调用它:

val oranges = fruits.filterIsInstance<Orange>()

传递具象化参数作为参数

具象化修饰符使得函数可以将类型形参作为类型参数传递给另一个具有具象化修饰符的函数:

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

inline fun <reified T> doSomethingElse() { }

获取具体化类型的泛型类型

有时类型参数可以是泛型类型。例如,函数调用doSomething<List<String>>()中的List<String>。由于物化,我们有可能了解整个类型:

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

这里typeOf()是一个标准库函数。上面的println()函数将打印kotlin.collections. list <kotlin. list。String>,如果调用getGenericType<List<String>>()。KType包括KClass、类型参数信息和可空性信息。一旦知道了KType,就可以对其执行反射。


Java的互操作性

没有具体化类型参数声明的内联函数可以作为常规Java函数从Java中调用。但是用具体化的类型参数声明的对象不能从Java调用。

即使你像下面这样使用反射调用它:

Method method = YourFilenameKt.class.getDeclaredMethod("doSomething", Object.class);
method.invoke("hello", Object.class);

您将得到UnsupportedOperationException:该函数具有具体化的类型参数,因此只能在编译时内联,不能直接调用。


结论

在许多情况下,具体化的类型可以帮助我们消除以下错误和警告:

错误:不能使用“T”作为具体化的类型参数。使用类代替。 错误:无法检查已擦除类型:T的实例 警告:未检查强制转换:SomeType到T