Scala:数组和类型擦除

Posted

技术标签:

【中文标题】Scala:数组和类型擦除【英文标题】:Scala: arrays and type erasure 【发布时间】:2012-12-30 13:49:37 【问题描述】:

我想编写如下重载函数:

case class A[T](t: T)
def f[T](t: T) = println("normal type")
def f[T](a: A[T]) = println("A type")

结果如我所料:

f(5)       => 普通类型 f(A(5))  => A 类型

到目前为止一切顺利。但问题是同样的事情不适用于数组:

def f[T](t: T) = println("normal type")
def f[T](a: Array[T]) = println("Array type")

现在编译器抱怨:

双重定义:第 14 行的方法 f:[T](t: Array[T])Unit 和方法 f:[T](t: T)Unit 在擦除后具有相同的类型: (t: java.lang.对象)单位

我认为类型擦除后第二个函数的签名应该是 (a: Array[Object])Unit 而不是 (t: Object)Unit,所以它们不应该相互冲突。我在这里错过了什么?

如果我做错了什么,写 f 的正确方法是什么,以便根据参数的类型调用正确的方法?

【问题讨论】:

【参考方案1】:

这在 Java 中从来不是问题,因为它不支持泛型中的原始类型。因此,以下代码在 Java 中是相当合法的:

public static <T> void f(T t)out.println("normal type");
public static <T> void f(T[] a)out.println("Array type");

另一方面,Scala 支持所有类型的泛型。尽管 Scala 语言没有原语,但生成的字节码将它们用于 Int、Float、Char 和 Boolean 等类型。它使 Java 代码和 Scala 代码有所不同。 Java 代码不接受int[] 作为数组,因为int 不是java.lang.Object。所以Java可以将这些方法参数类型擦除为ObjectObject[]。 (这意味着 JVM 上的 Ljava/lang/Object;[Ljava/lang/Object;。)

另一方面,您的 Scala 代码处理所有数组,包括 Array[Int]Array[Float]Array[Char]Array[Boolean] 等。这些数组是(或可以是)原始类型的数组。它们不能在 JVM 级别转换为 Array[Object]Array[anything else]Array[Int]Array[Char] 只有一个超类型:它是 java.lang.Object。它是您可能希望拥有的更通用的超类型。

为了支持这些陈述,我编写了一个不那么通用的方法 f 的代码:

def f[T](t: T) = println("normal type")
def f[T <: AnyRef](a: Array[T]) = println("Array type")

此变体的工作方式类似于 Java 代码。这意味着,不支持原语数组。但是这个小改动足以让它编译。另一方面,由于类型擦除原因,无法编译以下代码:

def f[T](t: T) = println("normal type")
def f[T <: AnyVal](a: Array[T]) = println("Array type")

添加@specialized并不能解决问题,因为生成了一个泛型方法:

def f[T](t: T) = println("normal type")
def f[@specialized T <: AnyVal](a: Array[T]) = println("Array type")

我希望@specialized 可能已经解决了这个问题(在某些情况下),但编译器目前不支持它。但我认为这不会是 scalac 的高优先级增强。

【讨论】:

太棒了!您的解释消除了我所有未回答的问题。我同意你的看法,如果@specialized 解决了这个问题就好了。 +1,特别是为了提醒 Java 不支持泛型中的原始类型(我倾向于忘记它,Scala 太多了 :))。这确实是一个重要的事实,因为这意味着在 Java 中,所有数组类型都可以有效地擦除到Array[Object],因为所有非原始类型都可以擦除到Object。基本类型的数组(例如Array[Int],或者更确切地说是int[])永远不能被强制转换为Array[Object]Array[T]【参考方案2】:

我认为类型擦除后第二个函数的签名应该是 (a: Array[Object])Unit 而不是 (t: Object)Unit,所以它们不应该相互冲突。我在这里错过了什么?

擦除恰恰意味着您丢失了有关泛型类的类型参数的任何信息,并且仅获得原始类型。所以def f[T](a: Array[T])的签名不能是def f[T](a: Array[Object]),因为你还有一个类型参数(Object)。根据经验,您只需删除类型参数即可获得擦除类型,这将为我们提供def f[T](a: Array)。这适用于所有其他泛型类,但数组在 JVM 上是特殊的,,特别是它们的擦除只是Object(没有array 原始类型)。因此删除后f的签名确实是def f[T](a: Object) [更新,我错了]实际上在检查了java规范之后,看来我在这里完全错了。规范说

数组类型T[]的擦除是|T|[]

其中|T|T 的擦除。所以,数组确实被特殊对待,但奇怪的是,虽然类型参数确实被删除了,但类型被标记为 T 的数组,而不仅仅是 T。 这意味着Array[Int] 在擦除之后仍然是Array[Int]。 但Array[T] 不同:T 是泛型方法f 的类型参数。为了能够通用地处理任何类型的数组,scala 除了将Array[T] 转换为Object 之外别无选择(顺便说一下,我认为Java 也是如此)。 这是因为正如我上面所说,没有原始类型Array 这样的东西,所以它必须是Object

我会尝试换一种说法。通常在编译带有MyGenericClass[T] 类型参数的泛型方法时,仅擦除类型为MyGenericClass 的事实就可以(在JVM 级别)传递MyGenericClass 的任何实例化,例如MyGenericClass[Int]MyGenericClass[Float],因为它们在运行时实际上都是一样的。然而,这不适用于数组:Array[Int] 是与Array[Float] 完全无关的类型,它们不会擦除为常见的Array 原始类型。它们最不常见的类型是Object,因此这是在对数组进行一般处理时在后台操作的内容(编译器无法静态知道元素的类型)。

更新 2:v6ak 的回答添加了一些有用的信息:Java 不支持泛型中的原始类型。所以在Array[T] 中,T 必然是(在 Java 中,但不是在 Scala 中)Object 的子类,因此它对Array[Object] 的擦除完全有意义,不像在 Scala 中 T 可以举例是原始类型Int,它绝对不是Object(又名AnyRef)的子类。为了和Java一样,我们可以用一个上限来约束T,果然,现在它编译得很好:

def f[T](t: T) = println("normal type")
def f[T<:AnyRef](a: Array[T]) = println("Array type") // no conflict anymore

关于如何解决该问题,一个常见的解决方案是添加一个虚拟参数。因为您当然不想在每次调用时显式传递一个虚拟值,所以您可以给它一个虚拟默认值,或者使用编译器总是会隐式找到的隐式参数(例如 dummyImplicit 在 @ 987654360@):

def f[T](a: Array[T], dummy: Int = 0)
// or:
def f[T](a: Array[T])(implicit dummy: DummyImplicit)
// or:
def f[T:ClassManifest](a: Array[T])

【讨论】:

+1 因为它解释了问题而不是简单地缓解它 感谢@Régis 的精彩回答。这很有说服力,但仍然存在一个问题。我刚刚尝试了f(a: Array[Int]),结果证明可以与f[T](t: T) 共存。如果f(a: Array[Int]) 可以在类型擦除后继续存在,那么很难找到f[T](a: Array[T]) 不应该存在的原因。 :// 你是对的,实际上我在上面的一个陈述中是不正确的。我已经编辑了我的答案。简短的故事:数组实际上并没有擦除到 Object,但是泛型数组必须(在后台)被视为纯粹的 Object 实例。 如果规范说数组类型 T[] 的擦除是 |T|[],这并不完全意味着 Array[T] 的擦除是 Array[Object] 而不是 Object,因为|T|是对象? 的含义是因为我上面说过没有原始类型Array这样的东西,所以它必须是Object。对我来说似乎还不清楚.. 如果 JVM 对数组进行特殊处理,并在擦除后尝试保留更多的类型信息不少于任何其他泛型类型,则应按照规范将其转换为 Array[Object]。变成 Object 完全丢失了类型信息,即使是普通的泛型类型也被“处理得更好”,即 A[T] 变成 A 而不是 Object..【参考方案3】:

[Scala 2.9] 一种解决方案是使用隐式参数,这些参数自然地修改方法的签名,使它们不会发生冲突。

case class A()

def f[T](t: T) = println("normal type")
def f[T : Manifest](a: Array[T]) = println("Array type")

f(A())        // normal type
f(Array(A())) // Array type

T : Manifest 是第二个参数列表(implicit mf: Manifest[T]) 的语法糖。

不幸的是,我不知道为什么 Array[T] 会被删除为 Object 而不是 Array[Object]

【讨论】:

但是类型擦除后可以有 Array[Object] 吗? List[T] 被擦除为List[Object],所以Array[T] 应该被擦除为Array[Object]。然而,正如 Régis Jean-Gilles 所指出的,JVM 对数组进行了特殊处理,因此似乎不存在 Array 的无类型参数版本。 感谢您的回答,mhs。这可能是一种可能的解决方法,尽管它实际上不需要是 Manifest。任何虚拟隐式参数,例如@Régis 建议的 DummyImplicit 可以区分这两个功能。谢谢!【参考方案4】:

要克服 scala 中的类型擦除,您可以添加一个隐式参数,该参数将为您提供 Manifest (scala 2.9.*) 或 TypeTag (scala 2.10),然后您可以获得有关类型如下:

def f[T](t: T)(隐式清单:Manifest[T])

您可以检查 m 是否是 Array 等的实例。

【讨论】:

感谢您的回答,阿蒙。似乎它不需要是 Manifest,尽管它可能对函数内部的后续调用很有用。 DummyImplicit 可能会像 Régis 建议的那样在这里做。谢谢!

以上是关于Scala:数组和类型擦除的主要内容,如果未能解决你的问题,请参考以下文章

如何绕过 Scala 上的类型擦除?或者,为啥我不能获取我的集合的类型参数?

Scala双重定义(2个方法具有相同的类型擦除)

Scala:抽象类型模式 A 未选中,因为它已被擦除消除

在Scala中对列表/序列进行模式匹配时解决类型擦除问题

Kotlin的类型具体化在Java或Scala中是不可能实现的?

Scala的类与类型