首页 文章

Scala:数组和类型擦除

提问于
浏览
10

我想编写重载函数如下:

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")

现在编译器抱怨:

double definition:方法f:[T](t:Array [T])单位和方法f:[T](t:T)第14行的单位在擦除后具有相同的类型:(t:java.lang.Object)单元

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

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

4 回答

  • 3

    这在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的高优先级增强 .

  • 8

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

    擦除精确意味着您丢失了有关泛型类的类型参数的任何信息,并且只获取原始类型 . 所以 def f[T](a: Array[T]) 的签名不能是 def f[T](a: Array[Object]) ,因为你仍然有一个类型参数( Object ) . 根据经验,你只需要删除类型参数以获得擦除类型,这将给我们 def f[T](a: Array) . 这适用于所有其他泛型类,但是数组在JVM上是特殊的,特别是它们的擦除只是Object(其中没有数组原始类型) . 因此擦除后f的签名确实是def f [T](a:Object) . [Updated, I was wrong] 实际上在检查了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 ,因此当数组被一般地处理时,这是在引擎盖下操作的(每个编译器都不能静态地知道元素的类型) .

    UPDATE 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
    

    至于如何解决问题,一个常见的解决方案是添加一个虚拟参数 . 因为您当然不希望在每次调用时显式传递虚拟值,您可以为其指定一个虚拟默认值,或者使用始终由编译器隐式找到的隐式参数(例如 Predef 中的 dummyImplicit ):

    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])
    
  • 8

    [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] .

  • 3

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

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

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

相关问题