11

我想编写如下重载函数:

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.Object)Unit

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

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

4

4 回答 4

9

这在 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]在 JVM 级别上。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 的高优先级增强。

于 2013-01-16T13:02:24.090 回答
8

我认为类型擦除后第二个函数的签名应该是 (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

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

更新 2:v6ak 的回答添加了一些有用的信息:Java 不支持泛型中的原始类型。所以 in 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找到的Predef):

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])
于 2013-01-16T09:51:30.580 回答
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].

于 2013-01-16T09:41:01.903 回答
3

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

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

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

于 2013-01-16T09:43:57.737 回答