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可以将这些方法参数类型擦除为Object
和Object[]
。 (这意味着 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 上是特殊的,,特别是它们的擦除只是
[更新,我错了]实际上在检查了java规范之后,看来我在这里完全错了。规范说 Object
(没有array
原始类型)。因此删除后f
的签名确实是def f[T](a: Object)
。
数组类型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 上的类型擦除?或者,为啥我不能获取我的集合的类型参数?