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

Posted

技术标签:

【中文标题】Scala双重定义(2个方法具有相同的类型擦除)【英文标题】:Scala double definition (2 methods have the same type erasure) 【发布时间】:2011-03-19 10:58:06 【问题描述】:

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

谢谢。

【问题讨论】:

另见***.com/questions/3422336/… 有谁知道为什么 Scala 不自动创建不同的擦除名称?如果您从 Scala 外部调用这些方法,这些方法在答案中提供了解决方法,您将需要知道要传递哪个隐式参数才能获得所需的方法。如果从 Scala 外部调用,这与需要手动知道要调用哪个自动修改的方法名称在性质上有何不同?自动修改的名称会更有效率,并消除所有这些样板!总有一天,我会在 scala-debate 上提问。 【参考方案1】:

我喜欢 Michael Krä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)

【讨论】:

你能解释一下 X:ClassManifest 这一行吗? @Mikaël Mayer 我使用未使用的 X 类型对方法进行参数化。因为它确实未使用,所以这不会改变 JVM 看到的签名(因为类型擦除)。但是,如果您使用清单在运行时跟踪此类类型,您会在内部为它们获得额外的参数,因此 JVM “看到”类似于 foo(List l,Classmanifest cx)foo(List l,Classmanifest cx, Classmanifest cy) 的东西,这与 foo(List l) 不同。 不错!您能否将您的第一个解决方案(即基于 case classs)与 Jean-Philippe Pellet 提供的解决方案进行比较? @Daniel 案例类解决方案需要更多的输入,但可以更好地扩展。此外,它不会增加参数列表的长度。在我看来,它不那么“hacky”。缺点是你用隐式转换“污染”了你的上下文,增加了类的数量,并且你需要在方法中“解包”你的参数。我认为这两种解决方案之间没有重大的性能差异。 ClassManifest 是 Scala 2.10 之前的解决方案,此后有 TypeTagClassTag。你能用 plz 更新你的解决方案吗?更多信息:docs.scala-lang.org/overviews/reflection/…【参考方案2】:

您可以使用在Predef 中定义的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) = ()

【讨论】:

在我的情况下不起作用。 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") ^ 想法? 您的两种方法都将String 作为非隐式参数。在我的示例中,参数的类型不同:List[String]List[Int]List[Date] 这实际上只是为我解决了一个完全不同的问题,谢谢:) 不错!您能否将此与 Landei 基于case classs 的解决方案进行比较? 你觉得哪个更好?【参考方案3】:

要理解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 类的私有对象,它也可以工作。

【讨论】:

如果我将 dummyN 标记为私有并将实现放在 foo 方法中,我会在执行它们时得到 java.lang.NoSuchMethodError。 天才!我已经有一个隐式参数,所以我将它们组合起来:(implicit c: MyContext, d: dummy1.type) 我认为这是最好的答案,并建议原来的提问者重新考虑。【参考方案4】:

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

【讨论】:

投反对票,因为作者(至少在问题的当前形式中)承认他们理解原因 - 但寻求更好的解决方法。【参考方案5】:

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

【讨论】:

这对我来说似乎是一个可怕的组合,不值得努力。一个更好的组合(仍然值得怀疑)是使用具有默认值的参数来区分这两种方法。 @peletom:您的方法(默认参数)无法编译并出现错误“方法 foo 的多个重载替代定义默认参数。”【参考方案6】:

如果我在这里结合Daniels response 和Sandor Murakozis 响应,我会得到:

@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带参数。

【讨论】:

【参考方案7】:

还有(至少一种)另一种方式,即使它不太好并且不是真正的打字安全:

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.

实际情况是 manifest 参数可以返回 T 擦除前的值。然后基于 T 的简单分派到各种实际实现即可完成剩下的工作。

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

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

当然,这一切也不是太好,我不确定是否值得这样做,尤其是如果你很懒;-)

【讨论】:

【参考方案8】:

除了使用清单之外,您还可以使用以类似方式隐式导入的调度程序对象。我在清单出现之前写了一篇博客:http://michid.wordpress.com/code/implicit-double-dispatch-revisited/

这具有类型安全的优点:重载的方法只能被具有已导入当前范围的调度程序的类型调用。

【讨论】:

【参考方案9】:

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

[...]

【讨论】:

我对提供副本 Aaron Novstrup 的答案投了反对票。我猜你没有检查他是否比你早两年在这里提供了答案。【参考方案10】:

我尝试改进 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 的答案不适用于值类。

【讨论】:

【参考方案11】:

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

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如何强制执行类型绑定呢? one link 说类型绑定保留在编译器应该访问的元数据中。这解释了为什么使用类型边界不会启用重载。这也意味着 JVM 是一个开放的安全漏洞,因为可以在没有类型边界的情况下调用类型有界的方法(哎呀!),所以请原谅我假设 JVM 设计者不会做这种不安全的事情。

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

【讨论】:

如果您对此投反对票,请添加或发送评论给我解释。我认为这适用于最新版本?我暂时没有安装它来测试。理论上上限,应该是擦除到的类型,但我认为这只是在 2.8 中完成的。 我应用的假设这必须有效的逻辑是,如果上限仅应用于函数内部的强制转换而不应用于函数的签名,那么这意味着可以调用函数 (由Java)类型不匹配。或者,如果您更喜欢只使用显式存在类型,afaics 就是上限所暗示的,def foo(s: List[_ <: string> 我的想法可能行不通,不是因为类型擦除本身,而是因为 JVM 擦除的类型信息比类型擦除的最佳操作所必需的要多,因此显然 JVM 不知道 List 之间的区别[T] 和 List[T <: string list> 这个cakoose.com/wiki/type_erasure_is_not_evil 链接说明JVM 丢弃了太多类型信息,因此没有实现最佳类型擦除。我建议的想法是基于 JVM 以最佳方式进行类型擦除的假设。 你不能只是编造一些东西并期望得到认真对待和评论。不过,您可能需要考虑 JVM 从类型构造函数中删除类型变量这一事实。正确的解决方案是使用 sum 类型。始终避免在 Scala 中重载。

以上是关于Scala双重定义(2个方法具有相同的类型擦除)的主要内容,如果未能解决你的问题,请参考以下文章

Scala:尝试了扩展方法,但无法完全构造(多个类上的扩展名相同)

scala基本数据类型

Scala:如何在case类构造函数中使用类型作为第一类值?

在向量中使用擦除时双重释放或损坏(fasttop)。知道它们的索引,你怎么能擦除向量的几个项目?

在Scala中对列表/序列进行模式匹配时解决类型擦除问题

Scala:数组和类型擦除