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

时间:2010-07-22 09:30:26

标签: scala compilation overloading typeclass type-erasure

我在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]的类型不同?

感谢。

11 个答案:

答案 0 :(得分:48)

我喜欢MichaelKrämer使用含义的想法,但我认为它可以更直接地应用:

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: ClassManifest](p: List[Int]) { println("Ints") }
def foo[X: ClassManifest, Y: ClassManifest](p: List[Double]) { println("Doubles") }

对于每个版本,您需要一个额外的类型参数,因此这不会扩展,但我认为对于三个或四个版本来说没问题。

[更新2]

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

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

答案 1 :(得分:48)

您可以使用DummyImplicit中定义的Predef,而不是发明虚拟隐式值,而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) = () } 似乎完全是为了这一点:

{{1}}

答案 2 :(得分:10)

由于类型擦除的奇迹,您的方法列表的类型参数在编译期间被擦除,从而将两种方法都减少到相同的签名,这是编译器错误。

答案 3 :(得分:10)

要理解Michael Krämer's solution,必须认识到隐式参数的类型并不重要。 重要的是它们的类型是不同的。

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

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类的私有对象,它甚至可以工作。

答案 4 :(得分:8)

正如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)
}

感谢Michid提示!

答案 5 :(得分:6)

如果我在这里结合Daniel s responseSandor Murakozi的回复,我会得到:

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

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

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

逻辑也可以包含在类型类中(感谢jsuereth):     @ annotation.implicitNotFound(msg =“Foo不支持$ {T}只接受Int和String”)     密封特性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)

给出了:

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

答案 6 :(得分:3)

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

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

隐式清单参数可用于“重新统一”擦除类型,从而破解擦除。您可以在许多博客文章中了解更多信息,例如。 this one

发生的事情是,清单参数可以在删除之前回复T。然后基于T对各种实际实现进行简单的调度完成其余的工作。

可能有更好的方法来进行模式匹配,但我还没有看到它。人们通常做的是匹配m.toString,但我认为保持类更清晰(即使它更冗长)。不幸的是,Manifest的文档不是太详细,也许它还有一些可以简化它的东西。

它的一个很大的缺点是它不是真正的类型安全:foo会对任何T感到满意,如果你不能处理它你会遇到问题。我想它可以解决T上的一些约束,但它会使它进一步复杂化。

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

答案 7 :(得分:1)

答案 8 :(得分:0)

我从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
} 

[...]

答案 9 :(得分:0)

我尝试改进Aaron Novstrup和Leo的答案,使一组标准证据对象可导入且更简洁。

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
}

但是,值类不允许内部特征,类和对象。因此,请注意Aaron Novstrup和Leo的答案不适用于价值类。

答案 10 :(得分:-3)

我没有对此进行测试,但为什么上限不起作用?

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中读到,上限现在以这种方式编码,而不是始终是Any。

要在协变类型上重载,请使用不变的边界(在Scala中是否有这样的语法?...啊我认为没有,但请将以下内容作为上述主要解决方案的概念附录):

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

然后我假设在删除的代码版本中消除了隐式转换。


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

在我写这篇文章的时候,我不明白stackoverflow是一个按照一些竞争而不是声誉的答案质量对人们进行评级的系统。我认为这是一个分享信息的地方。在我写这篇文章的时候,我从概念层面(比较许多不同的语言)比较了具体化和非具体化,因此在我看来,删除类型边界没有任何意义。

相关问题