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

我用 scala 编写的这个程序无法编译:

class TestDoubleDef{
def foo(p:List[String]) = {}
def foo(p:List[Int]) = {}
}

编译器通知:

[error] double definition:
[error] method foo:(List[String])Unit and
[error] method foo:(List[Int])Unit at line 120
[error] have same type after erasure: (List)Unit

我知道 JVM 不支持泛型,所以我理解这个错误。

我可以为 List[String]List[Int]编写包装,但我很懒:)

我表示怀疑,但是,有没有其他表达 List[String]List[Int]不同类型的方法?

谢谢。

16963 次浏览

由于类型擦除的奇妙之处,在编译期间将擦除方法 List 的类型参数,从而将两个方法减少到相同的签名,这是编译器错误。

正如 Viktor Klang 已经说过的,泛型类型将被编译器擦除:

class TestDoubleDef{
def foo(p:List[String])(implicit ignore: String) = {}
def foo(p:List[Int])(implicit ignore: Int) = {}
}


object App extends Application {
implicit val x = 0
implicit val y = ""


val a = new A()
a.foo(1::2::Nil)
a.foo("a"::"b"::Nil)
}

感谢 米奇的提示!

还有(至少有一种)另一种方法,即使它不太好,也不是真正的类型安全:

import scala.reflect.Manifest


object Reified {


def foo[T](p:List[T])(implicit m: Manifest[T]) = {


def stringList(l: List[String]) {
println("Strings")
}
def intList(l: List[Int]) {
println("Ints")
}


val StringClass = classOf[String]
val IntClass = classOf[Int]


m.erasure match {
case StringClass => stringList(p.asInstanceOf[List[String]])
case IntClass => intList(p.asInstanceOf[List[Int]])
case _ => error("???")
}
}




def main(args: Array[String]) {
foo(List("String"))
foo(List(1, 2, 3))
}
}

隐式清单参数可以用来“实现”擦除类型,从而绕过擦除。你可以在很多博客文章中了解更多,比如 这个

清单参数可以返回删除之前的 T。然后,基于 T 对各种实际实现进行简单的分派,完成剩下的工作。

也许还有更好的模式匹配但我还没见过。人们通常做的是在 m.toString 上进行匹配,但是我认为保持类比较干净(即使有点冗长)。不幸的是 Manifest 的文档不太详细,也许它也有一些可以简化它的东西。

它的一个很大的缺点是它不是真正的类型安全的: foo 对任何 T 都很满意,如果你不能处理它,你就会有问题。我想它可以通过对 T 的一些约束来处理,但是这会使它更加复杂。

当然,这整件事情也不是太好,我不确定它是否值得做,特别是如果你是懒惰的; -)

我喜欢迈克尔•克雷默(Michael Kramer)使用隐含意义的想法,但我认为它可以更直接地应用:

case class IntList(list: List[Int])
case class StringList(list: List[String])


implicit def il(list: List[Int]) = IntList(list)
implicit def sl(list: List[String]) = StringList(list)


def foo(i: IntList) { println("Int: " + i.list)}
def foo(s: StringList) { println("String: " + s.list)}

我认为这是相当可读和直接。

[更新]

还有一种简单的方法似乎奏效:

def foo(p: List[String]) { println("Strings") }
def foo[X: ClassTag](p: List[Int]) { println("Ints") }
def foo[X: ClassTag, Y: ClassTag](p: List[Double]) { println("Doubles") }

对于每个版本,您都需要一个额外的类型参数,所以这不能伸缩,但是我认为对于三个或四个版本,这是可以的。

[更新2]

对于正好两种方法,我发现了另一个不错的技巧:

def foo(list: => List[Int]) = { println("Int-List " + list)}
def foo(list: List[String]) = { println("String-List " + list)}

您也可以使用以类似方式隐式导入的调度程序对象来代替使用清单。在货单出现之前,我在博客上写过这个: http://michid.wordpress.com/code/implicit-double-dispatch-revisited/

这具有类型安全的优点: 重载方法只能对将调度程序导入当前作用域的类型进行调用。

要理解 Michael Kramer 的解决方案,有必要认识到隐式参数的类型并不重要。重要的是它们的类型是不同的。

以下代码的工作方式相同:

class TestDoubleDef {
object dummy1 { implicit val dummy: dummy1.type = this }
object dummy2 { implicit val dummy: dummy2.type = this }


def foo(p:List[String])(implicit d: dummy1.type) = {}
def foo(p:List[Int])(implicit d: dummy2.type) = {}
}


object App extends Application {
val a = new TestDoubleDef()
a.foo(1::2::Nil)
a.foo("a"::"b"::Nil)
}

在字节码级别上,这两个 foo方法都变成了双参数方法,因为 JVM 字节码对隐式参数或多个参数列表一无所知。在调用站点,Scala 编译器通过查看传入的列表的类型来选择要调用的适当的 foo方法(因此也是要传入的适当的虚拟对象)(直到后来才被擦除)。

虽然这种方法更加冗长,但它减轻了调用方提供隐式参数的负担。实际上,如果 DummyN 对象是 TestDoubleDef类的私有对象,它甚至可以工作。

如果我把 丹尼尔s回应山多尔 · 村子s 的响应结合起来,我得到:

@annotation.implicitNotFound(msg = "Type ${T} not supported only Int and String accepted")
sealed abstract class Acceptable[T]; object Acceptable {
implicit object IntOk extends Acceptable[Int]
implicit object StringOk extends Acceptable[String]
}


class TestDoubleDef {
def foo[A : Acceptable : Manifest](p:List[A]) =  {
val m = manifest[A]
if (m equals manifest[String]) {
println("String")
} else if (m equals manifest[Int]) {
println("Int")
}
}
}

我得到了一个类型安全(类型)变体

scala> val a = new TestDoubleDef
a: TestDoubleDef = TestDoubleDef@f3cc05f


scala> a.foo(List(1,2,3))
Int


scala> a.foo(List("test","testa"))
String


scala> a.foo(List(1L,2L,3L))
<console>:21: error: Type Long not supported only Int and String accepted
a.foo(List(1L,2L,3L))
^


scala> a.foo("test")
<console>:9: error: type mismatch;
found   : java.lang.String("test")
required: List[?]
a.foo("test")
^

逻辑也可以包含在类型类中(感谢 耶稣) : (msg = “ Foo 不支持 ${ T }只接受 Int 和 String”) 密封特性 Foo [ T ]{ def application (List: List [ T ]) : Unit }

object Foo {
implicit def stringImpl = new Foo[String] {
def apply(list : List[String]) = println("String")
}
implicit def intImpl = new Foo[Int] {
def apply(list : List[Int]) =  println("Int")
}
}


def foo[A : Foo](x : List[A]) = implicitly[Foo[A]].apply(x)

结果是:

scala> @annotation.implicitNotFound(msg = "Foo does not support ${T} only Int and String accepted")
| sealed trait Foo[T] { def apply(list : List[T]) : Unit }; object Foo {
|         implicit def stringImpl = new Foo[String] {
|           def apply(list : List[String]) = println("String")
|         }
|         implicit def intImpl = new Foo[Int] {
|           def apply(list : List[Int]) =  println("Int")
|         }
|       } ; def foo[A : Foo](x : List[A]) = implicitly[Foo[A]].apply(x)
defined trait Foo
defined module Foo
foo: [A](x: List[A])(implicit evidence$1: Foo[A])Unit


scala> foo(1)
<console>:8: error: type mismatch;
found   : Int(1)
required: List[?]
foo(1)
^
scala> foo(List(1,2,3))
Int
scala> foo(List("a","b","c"))
String
scala> foo(List(1.0))
<console>:32: error: Foo does not support Double only Int and String accepted
foo(List(1.0))
^

注意,我们必须编写 implicitly[Foo[A]].apply(x),因为编译器认为 implicitly[Foo[A]](x)意味着我们使用参数调用 implicitly

我没有测试这个,但是为什么上限不起作用呢?

def foo[T <: String](s: List[T]) { println("Strings: " + s) }
def foo[T <: Int](i: List[T]) { println("Ints: " + i) }

从 foo (List [ Any ] s)到 foo (List [ String ] s)和 foo (List [ Int ] i)的擦除转换是否改变了两次:

Http://www.angelikalanger.com/genericsfaq/faqsections/technicaldetails.html#faq108

我想我在2.8版本中读到过,上界现在是这样编码的,而不是一直是任意的。

要重载协变类型,使用不变界(Scala 中有这样的语法吗?啊,我认为没有,但是把以下内容作为上述主要解决方案的概念性补充) :

def foo[T : String](s: List[T]) { println("Strings: " + s) }
def foo[T : String2](s: List[T]) { println("String2s: " + s) }

那么我假设隐式强制转换在代码的擦除版本中被消除了。


更新: 问题是 JVM 在方法签名上擦除的类型信息比“必需的”要多。我提供了一个链接。它从类型构造函数中删除类型变量,甚至删除这些类型变量的具体界限。这里有一个概念上的区别,因为擦除函数的类型绑定没有概念上的非实体化优势,这在编译时就已经知道了,并且不会随着泛型的任何实例而改变,而且调用方有必要不要调用类型不符合类型绑定的函数,所以如果类型绑定被擦除,JVM 如何强制执行类型绑定呢?一个环节说类型绑定保留在编译器应该访问的元数据中。这就解释了为什么使用类型界限不能启用重载。这还意味着 JVM 是一个广泛开放的安全漏洞,因为可以在没有类型界限的情况下调用类型绑定方法(呀!)因此,请原谅我假设 JVM 设计器不会做这种不安全的事情。

在我写这篇文章的时候,我并不理解堆栈溢出是一种根据答案的质量给人们打分的系统,就像一些名誉竞争一样。我以为那是个分享信息的地方。在我写这篇文章的时候,我正在从概念层面上比较具体化和非具体化(比较许多不同的语言) ,因此在我看来,删除类型绑定没有任何意义。

你可以使用在 Predef中定义的 DummyImplicit,而不是发明虚拟的隐式值,DummyImplicit看起来就是为此而生的:

class TestMultipleDef {
def foo(p:List[String]) = ()
def foo(p:List[Int])(implicit d: DummyImplicit) = ()
def foo(p:List[java.util.Date])(implicit d1: DummyImplicit, d2: DummyImplicit) = ()
}

我在“双重定义的消除歧义”http://scala-programming-language.1934581.n4.nabble.com/disambiguation-of-double-definition-resulting-from-generic-type-erasure-td2327664.html 中发现了一个不错的技巧 作者: Aaron Novstrup

再打败这匹死马。

我突然想到,一个更干净的黑客是使用一个独特的虚拟类型 对于签名中具有擦除类型的每个方法:

object Baz {
private object dummy1 { implicit val dummy: dummy1.type = this }
private object dummy2 { implicit val dummy: dummy2.type = this }


def foo(xs: String*)(implicit e: dummy1.type) = 1
def foo(xs: Int*)(implicit e: dummy2.type) = 2
}

[...]

我尝试改进亚伦 · 诺夫斯特拉普和利奥的答案,使得一套标准的证据对象可以输入,并且更加简洁。

final object ErasureEvidence {
class E1 private[ErasureEvidence]()
class E2 private[ErasureEvidence]()
implicit final val e1 = new E1
implicit final val e2 = new E2
}
import ErasureEvidence._


class Baz {
def foo(xs: String*)(implicit e:E1) = 1
def foo(xs: Int*)(implicit e:E2) = 2
}

但是,这将导致编译器抱怨,当 foo调用另一个需要相同类型的隐式参数的方法时,隐式值的选择不明确。

因此,我只提供以下几点,在某些情况下更为简洁。这种改进适用于值类(extend AnyVal)。

final object ErasureEvidence {
class E1[T] private[ErasureEvidence]()
class E2[T] private[ErasureEvidence]()
implicit def e1[T] = new E1[T]
implicit def e2[T] = new E2[T]
}
import ErasureEvidence._


class Baz {
def foo(xs: String*)(implicit e:E1[Baz]) = 1
def foo(xs: Int*)(implicit e:E2[Baz]) = 2
}

如果包含的类型名相当长,则声明一个内部 trait以使其更简洁。

class Supercalifragilisticexpialidocious[A,B,C,D,E,F,G,H,I,J,K,L,M] {
private trait E
def foo(xs: String*)(implicit e:E1[E]) = 1
def foo(xs: Int*)(implicit e:E2[E]) = 2
}

但是,值类不允许内部特征、类或对象。因此,也注意到亚伦诺夫斯特拉普和利奥的答案不适用于价值类。