Kotlin 泛型 Array<T> 导致“不能将 T 用作具体类型参数。请改用类”但 List<T> 不会

Posted

技术标签:

【中文标题】Kotlin 泛型 Array<T> 导致“不能将 T 用作具体类型参数。请改用类”但 List<T> 不会【英文标题】:Kotlin generics Array<T> results in "Cannot use T as a reified type parameter. Use a class instead" but List<T> does not 【发布时间】:2022-01-17 02:36:17 【问题描述】:

我有一个接口,其中包含 T 的数组(或列表)和一些元数据。

interface DataWithMetadata<T> 
    val someMetadata: Int
    fun getData(): Array<T>

如果我编写接口的最简单实现,我会在emptyArray() 上得到一个编译错误:“Cannot use T as a reified type parameter. Use a class instead.”

class ArrayWithMetadata<T>(override val someMetadata: Int): DataWithMetadata<T> 
    private var myData: Array<T> = emptyArray()

    override fun getData(): Array<T> 
        return myData
    

    fun addData(moreData: Array<T>) 
        this.myData += moreData
    

但是,如果我将接口和实现都更改为列表,则不会出现编译时问题:

interface DataWithMetadata<T> 
    val someMetadata: Int
    fun getData(): List<T>


class ListWithMetadata<T>(override val someMetadata: Int): DataWithMetadata<T> 
    private var myData: List<T> = emptyList()

    override fun getData(): List<T> 
        return myData
    

    fun addData(moreData: Array<T>) 
        this.myData += moreData
    

我怀疑我的问题中有一些关于 Kotlin 泛型的有趣课程。谁能告诉我编译器在幕后做了什么以及为什么 Array 失败但 List 没有?有没有一种惯用的方法可以在这种情况下编译 Array 实现?

额外问题:我使用 Array over List 的唯一原因是我经常看到 Kotlin 开发人员偏爱数组。是这样吗,如果是,为什么?

【问题讨论】:

【参考方案1】:

查看 kotlin stdlib (jvm) 中emptyArray() 的声明,我们注意到reified 类型参数:

public inline fun <reified @PureReifiable T> emptyArray(): Array<T>

reified 类型参数意味着你可以在编译时访问T 的类,并且可以像T::class 一样访问它。您可以在Kotlin reference 中阅读有关reified 类型参数的更多信息。由于Array&lt;T&gt; 编译为java T[],我们需要在编译时知道类型,因此需要reified 参数。如果你尝试编写一个没有 reified 关键字的 emptyArray() 函数,你会得到一个编译器错误:

fun <T> emptyArray() : Array<T> = Array(0,  throw Exception() )

不能将 T 用作具体类型参数。改用类。


现在,我们来看看emptyList()的实现:

public fun <T> emptyList(): List<T> = EmptyList

这个实现根本不需要参数T。它只返回内部对象EmptyList,它本身继承自List&lt;Nothing&gt;。 kotlin 类型 Nothingthrow 关键字的返回类型,并且是永远不存在的值 (reference)。如果一个方法返回Nothing,就相当于在那个地方抛出异常。所以我们可以在这里安全地使用Nothing,因为每次我们调用EmptyList.get(),编译器都知道这会返回一个异常。


额外问题:

来自 Java 和 C++,我习惯于使用 ArrayListstd::vector 更容易使用这些数组。我现在使用 kotlin 几个月了,在编写源代码时,我通常看不到数组和列表之间有很大的区别。两者都有大量有用的扩展函数,它们的行为方式相似。然而,Kotlin 编译器处理数组和列表的方式非常不同,因为 Java 互操作性对 Kotlin 团队非常重要。我通常更喜欢使用列表,这也是我在你的情况下推荐的。

【讨论】:

【参考方案2】:

问题在于Array 的泛型类型必须在编译时 已知,这由此处的reified 类型参数指示,如声明中所示:

public inline fun <reified @PureReifiable T> emptyArray(): Array<T>

只能创建像Array&lt;String&gt;Array&lt;Int&gt; 这样的具体数组,但不能创建Array&lt;T&gt; 类型的数组。

在这个answer 中,您可以找到几种解决方法。

【讨论】:

【参考方案3】:

最适合我的解决方法是:

@Suppress("UNCHECKED_CAST")
var pool: Array<T?> = arrayOfNulls<Any?>(initialCapacity) as Array<T?>

【讨论】:

【参考方案4】:

我在尝试返回 T 时得到了Type parameter T cannot be called as function

private fun <T> getData(): T 
    return T()

见What is the proper way to create new instance of generic class in kotlin?:

private fun <T> create(
    method: (Int) -> T,
    value: Int
): T 
    return method(value) // Creates T(value).


// Usage:

create(::ClassName, 1)

ClassName 扩展 T

也许这会有所帮助:

private inline fun <reified T> getData(): T 
    return T::class.java.newInstance()

但就我而言,我必须返回T(parameter),而不是T(),所以,没有尝试。另见How to get class of generic type parameter in Kotlin。

【讨论】:

【参考方案5】:

我在上面的解决方案中遇到了一些问题。这是我使用来自kotlin-reflect 的typeOf 得出的结论:

@Suppress("UNCHECKED_CAST")
private inline fun <reified T> createArrayOfGeneric(): Array<T> 
    return java.lang.reflect.Array.newInstance(typeOf<T>().javaType as Class<*>, 10) as Array<T>

newInstance方法将java类型作为你从typeOf得到的泛型类,第二个参数是数组的长度。

【讨论】:

以上是关于Kotlin 泛型 Array<T> 导致“不能将 T 用作具体类型参数。请改用类”但 List<T> 不会的主要内容,如果未能解决你的问题,请参考以下文章

Kotlin:从泛型集合到泛型数组

Kotlin 泛型

Kotlin 五 泛型 枚举

Kotlin基础接口泛型

Kotlin泛型 ② ( 可变参数 vararg 关键字与泛型结合使用 | 使用 [] 运算符获取指定可变参数对象 )

Kotlin 判断泛型类型 reified