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


68

我在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]吗?

谢谢。



6
有谁知道为什么Scala不会自动创建其他已删除的名称?如果从Scala外部调用这些方法,并且在答案中提供了解决方法,则将需要知道要传递哪个隐式参数才能获取所需的方法。与从外部Scala调用时需要手动知道要调用的自动修改的方法名称相比,这在质量上有何不同?自动修改的名称将更加有效,并且消除了所有这些样板!总有一天,我会四处询问斯卡拉辩论。
谢尔比·摩尔

Answers:


52

我喜欢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: 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)}

1
您能否进一步解释X:ClassManifest行?
米凯尔·梅耶

1
@MikaëlMayer我使用未使用的类型X对方法进行参数化。由于它实际上是未使用的,因此不会更改JVM所看到的签名(由于类型擦除)。但是,如果使用清单来在运行时跟踪此类类型,则会在内部获得它们的附加参数,因此JVM“看到”的东西与foo(List l,Classmanifest cx)foo(List l,Classmanifest cx, Classmanifest cy)有所不同foo(List l)
兰代

真好!您能否将您的第一个解决方案(即基于case classs的解决方案)与Jean-Philippe Pellet提供的解决方案进行比较?
丹尼尔(Daniel)

@Daniel案例类解决方案需要更多的键入,但扩展性更好。此外,它不会增加参数列表的长度。我认为它不太“ hacky”。不利的一面是,您通过隐式转换“污染”了上下文,增加了类的数量,并且您需要“解开”方法内部的参数。我认为这两个解决方案之间在性能上没有主要区别。
兰代2015年

5
ClassManifest是2.10之前的Scala解决方案,因为存在TypeTagClassTag。您可以使用plz更新解决方案吗?更多信息:docs.scala-lang.org/overviews/reflection / ...
Jules Ivanic

54

您可以使用似乎完全针对此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
在我的情况下不起作用。scala: ambiguous reference to overloaded definition, both method apply in class DBObjectExtension of type [A](key: String)(implicit d: DummyImplicit)List[A] and method apply in class DBObjectExtension of type [A](key: String)(implicit d1: DummyImplicit, implicit d2: DummyImplicit)A match argument types (String) and expected result type List[String] val zzzz : List[String] = place("cats") ^有什么想法吗?
专家

您的两个方法都将aString作为非隐式参数。在我的例子中,参数的类型是不同的:List[String]List[Int]List[Date]
Jean-Philippe Pellet

这实际上为我解决了一个完全不同的问题,谢谢:)
埃里克·卡普伦

真好!您能否将其与基于的Landei解决方案进行比较case class
丹尼尔(Daniel)

您认为哪一种更好?
丹尼尔(Daniel)

12

要了解MichaelKrämer的解决方案,有必要认识到隐式参数的类型并不重要。什么重要的是,它们的类型是不同的。

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

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


如果我将dummyN标记为private并将实现放在foo方法中,则在执行它们时会收到java.lang.NoSuchMethodError。
oluies 2010年

天才!我已经在我的位置设置了隐式参数,因此将它们组合在一起:(implicit c: MyContext, d: dummy1.type)
Marcus Downing

我发现这是最好的答案,并建议重新考虑原始问题。
akauppi,2016年

11

由于类型擦除的奇妙之处,方法列表的类型参数在编译过程中会被擦除,从而将两个方法减少为相同的签名,这是编译器错误。


2
拒绝投票,是因为作者(至少以当前问题的形式)承认他们理解原因-但要求采取更好的方法。
akauppi,2016年

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的提示!


7
对我来说,这似乎是一个可怕的想法,不值得付出努力。更好的选择(仍然值得怀疑)是使用带有默认值的参数来区分这两种方法。
汤姆·克罗基特

3
@peletom:您的方法(具有默认参数)无法编译,并显示错误“方法foo的多个重载替代定义默认参数”。
肯·布鲁姆

6

如果将Daniel回应Sandor 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 (列表:列表[T]):单位}

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使用参数进行调用。


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

隐式清单参数器可用于“确定”已擦除的类型,从而消除擦除。您可以在许多博客文章(例如,这篇文章)中了解更多有关它的信息。

发生的事情是,清单参数可以将擦除前的T还给您。然后,将基于T的简单分派分配给各种实际实现。

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

它的一个很大的缺点是它不是真正的安全类型:foo对任何T都会感到满意,如果您不能处理它,则会遇到问题。我猜想它可以解决一些关于T的约束,但是会使它更加复杂。

当然,这整个过程也不太好,我不确定是否值得这样做,特别是如果您很懒的话;-)



0

我从 Aaron Novstrup的http://scala-programming-language.1934581.n4.nabble.com/disambiguation-of-double-definition-resulting-from-generic-type-erasure-td2327664.html中找到的好技巧

再打这匹死马...

在我看来,一个更干净的技巧是为每种方法使用唯一的伪类型,并在其签名中删除类型:

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
} 

[...]


2
我拒绝为亚伦·诺夫斯特鲁普(Aaron Novstrup)的答案提供副本。我猜您没有提前两年就看到他已经提供了答案。
谢尔比·摩尔

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的答案不适用于值类。


-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是一个开放的安全漏洞,因为可以在没有类型限制的情况下调用类型限制方法(yikes!),所以请您以为JVM设计人员不会做这种不安全的事情。

在我写这篇文章的时候,我还不知道stackoverflow是一种通过回答质量(例如声誉竞争)对人员进行评级的系统。我认为这是一个共享信息的地方。在我写这篇文章的时候,我是从概念层面(比较许多不同的语言)比较固定化和非固定化的,因此在我看来,删除类型绑定没有任何意义。


1
如果您对此表示反对,请添加或发送评论给我进行解释。我认为这适用于最新版本?我目前没有安装它进行测试。从理论上讲,应该将类型擦除为上限,但是我认为这仅在2.8中完成。
谢尔比·摩尔三世

我假设此逻辑必须起作用的逻辑是,如果仅对函数内部的强制转换应用上限,而对函数的签名不应用上限,那么这意味着该函数可能会被类型不匹配的Java调用。或者,如果您更喜欢只使用显式的存在类型,即afaics就是上限的含义,那么def foo(s:List [_ <:String])
Shelby Moore III

我的想法可能不起作用,不是因为类型擦除本身,而是因为JVM擦除了比类型擦除最佳操作所需的更多类型信息,因此,显然JVM不知道List [T]和List [T之间的区别<:字符串]参数(都只是List)。在某些方面,类型擦除比实现更合理/有效-所有具体实现都只需要1个运行时版本,但是为了避免不必要的强制转换(即反射),VM应该传播而不擦除类型参数的范围。
谢尔比·摩尔三世

1
这个cakoose.com/wiki/type_erasure_is_not_evil链接说明JVM丢弃了太多的类型信息,因此无法实现最佳的类型擦除。我提出的想法是基于JVM最佳地进行类型擦除的假设。
谢尔比·摩尔三世

3
您不能只是编造东西,期望得到认真的对待和评论。不过,您可能要考虑JVM从类型构造函数中删除类型变量的事实。正确的解决方案是使用总和类型。始终避免在Scala中超载。
Tony Morris
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.