【问题标题】:Scala - Why are overloaded methods not called based on runtime class?Scala - 为什么不基于运行时类调用重载方法?
【发布时间】:2014-04-15 18:34:33
【问题描述】:

问题

给定一个简单的类层次结构

abstract class Base {}
class A extends Base {}
class B extends Base {}

还有一个类型类

trait Show[T] {
  def show(obj: T): String
}

重载实现

class ShowBase extends Show[Base] {
  override def show(obj: Base): String = "Base"
}
object ShowA extends ShowBase {
  def show(obj: A): String = "A"
}
object ShowB extends ShowBase {
  def show(obj: B): String = "B"
}

执行以下测试用例时

Seq((new A(), ShowA), (new B(), ShowB)).foreach {
  case (obj, showImpl) => println(showImpl.show(obj), obj.getClass.getSimpleName)
}

应该产生(A,A) \n (B,B),但产生(Base,A) \n (Base,B)

问题

这里发生了什么?不应该调用具有最特定运行时类型的方法 - Polymorphism 101 吗?

此问题类似于another question,其中类型参数阻止正确解析要调用的方法。但是,在我的情况下,类型参数化的 show 方法与其他问题中的类型参数化方法相比,提供了实际实现。

天真的解决方案

扩展ShowA 实现(ShowB 的类似物):

object ShowA extends ShowBase {
  def show(obj: A): String = "A"
  override def show(obj: Base): String = {
    require(obj.isInstanceOf[A], "Argument must be instance of A!")
    show(obj.asInstanceOf[A])
  }
}

给出预期的输出。问题是将AShowB 混合会导致异常

【问题讨论】:

    标签: scala types overloading


    【解决方案1】:

    静态重载决议很容易推理:对于适用的方法,仅根据签名选择一个方法“更具体”。

    然而,

    scala> Seq((new A(), ShowA), (new B(), ShowB))
    res0: Seq[(Base, ShowBase)] = List((A@2b45f918,ShowA$@7ee4acd9), (B@57101ba4,ShowB$@6286d8a3))
    

    ShowBase 中没有过载。

    scala> res0 foreach {
         | case (obj: A, showImpl) => println(showImpl.show(obj), obj.getClass.getSimpleName)
         | case (obj: B, showImpl) => println(showImpl.show(obj), obj.getClass.getSimpleName)
         | }
    java.lang.InternalError: Malformed class name
      at java.lang.Class.getSimpleName(Class.java:1180)
      at $anonfun$1.apply(<console>:17)
      at $anonfun$1.apply(<console>:16)
      at scala.collection.immutable.List.foreach(List.scala:383)
      ... 38 elided
    

    哦,是的,不要使用来自 Scala 的 getSimpleName

    scala> res0 foreach {
         | case (obj: A, showImpl) => println(showImpl.show(obj), obj.getClass)
         | case (obj: B, showImpl) => println(showImpl.show(obj), obj.getClass) }
    (Base,class $line4.$read$$iw$$iw$A)
    (Base,class $line5.$read$$iw$$iw$B)
    

    哦,

    scala> class ShowBase extends Show[Base] {
         | override def show(obj: Base): String = "Base"
         | def show(a: A) = "A" ; def show(b: B) = "B" }
    defined class ShowBase
    
    scala> Seq((new A(), new ShowBase), (new B(), new ShowBase))
    res3: Seq[(Base, ShowBase)] = List((A@15c3e01a,ShowBase@6eadd61f), (B@56c4c5fd,ShowBase@10a2918c))
    
    scala> res3 foreach {
         | case (obj: A, showImpl) => println(showImpl.show(obj), obj.getClass)
         | case (obj: B, showImpl) => println(showImpl.show(obj), obj.getClass) }
    (A,class $line4.$read$$iw$$iw$A)
    (B,class $line5.$read$$iw$$iw$B)
    

    很容易想象一个宏会使用重载方法show为给定接口生成部分函数。

    另一个不一定很好的想法是让编译器在运行时进行选择。

    目前在 REPL 中演示这很尴尬。您必须从散布您的 REPL 历史记录的对象中导入您想要使用的任何符号。 See the issue.

    scala> def imps = $intp.definedSymbolList map (s => $intp.global.exitingTyper { s.fullName }) mkString ("import ", "\nimport ", "\n")
    imps: String
    
    scala> tb.eval(tb.parse(s"$imps ; ShowA show a"))
    res15: Any = A
    

    嘿,成功了!

    或者,进入电源模式,将当前阶段设置为 typer,并为您提供intp,没有时髦的美元符号。因为我们真的需要更多的钱吗?

    scala> :power
    ** Power User mode enabled - BEEP WHIR GYVE **
    ** :phase has been set to 'typer'.          **
    ** scala.tools.nsc._ has been imported      **
    ** global._, definitions._ also imported    **
    ** Try  :help, :vals, power.<tab>           **
    
    scala> def imps = intp.definedSymbolList map (_.fullName) mkString ("import ", "\nimport ", "\n")
    imps: String
    
    scala> tb.eval(tb.parse(s"$imps ; ShowA show a"))
    res17: Any = A
    

    如果您想查看未经处理的导入:

    scala> intp.isettings.unwrapStrings = false
    intp.isettings.unwrapStrings: Boolean = false
    
    scala> imps
    res11: String =
    "import $line2.$read.$iw.$iw.$intp
    import $line3.$read.$iw.$iw.Base
    import $line3.$read.$iw.$iw.A
    import $line3.$read.$iw.$iw.B
    import $line4.$read.$iw.$iw.Show
    import $line5.$read.$iw.$iw.ShowA
    [snip]
    

    再一次:

    scala> abstract class Base ; class A extends Base ; class B extends Base
    defined class Base
    defined class A
    defined class B
    
    scala> trait Show[T <: Base] { def show(obj: T): String }
    defined trait Show
    
    scala> class ShowBase extends Show[Base] { override def show(obj: Base): String = "Base" }
    defined class ShowBase
    
    scala> object ShowA extends ShowBase { def show(obj: A): String = "A" }
    defined object ShowA
    
    scala> :power
    ** Power User mode enabled - BEEP WHIR GYVE **
    ** :phase has been set to 'typer'.          **
    ** scala.tools.nsc._ has been imported      **
    ** global._, definitions._ also imported    **
    ** Try  :help, :vals, power.<tab>           **
    
    scala> def imps = intp.definedSymbolList map (_.fullName) mkString ("import ", "\nimport ", "\n")
    imps: String
    
    scala> import tools.reflect._
    import tools.reflect._
    
    scala> val tb = reflect.runtime.currentMirror.mkToolBox()
    tb: scala.tools.reflect.ToolBox[reflect.runtime.universe.type] = scala.tools.reflect.ToolBoxFactory$ToolBoxImpl@24e15d95
    

    我有没有提到导入机制很尴尬?

    scala> val a = new A
    a: A = A@1e5b2860
    
    scala> tb.eval(tb.parse(s"$imps ; ShowA show a"))
    res0: Any = A
    
    scala> ShowA show (a: Base)
    res1: String = Base
    
    scala> tb.eval(tb.parse(s"$imps ; ShowA show (a: Base)"))
    res2: Any = Base
    
    scala> val a: Base = new A
    a: Base = A@7e3a93ce
    
    scala> tb.eval(tb.parse(s"$imps ; ShowA show a"))
    scala.tools.reflect.ToolBoxError: reflective compilation has failed:
    
    reference to a is ambiguous;
    it is imported twice in the same scope by
    import a
    and import a
      at scala.tools.reflect.ToolBoxFactory$ToolBoxImpl$ToolBoxGlobal.throwIfErrors(ToolBoxFactory.scala:315)
      at scala.tools.reflect.ToolBoxFactory$ToolBoxImpl$ToolBoxGlobal.wrapInPackageAndCompile(ToolBoxFactory.scala:197)
      at scala.tools.reflect.ToolBoxFactory$ToolBoxImpl$ToolBoxGlobal.compile(ToolBoxFactory.scala:251)
      at scala.tools.reflect.ToolBoxFactory$ToolBoxImpl$$anonfun$compile$2.apply(ToolBoxFactory.scala:428)
      at scala.tools.reflect.ToolBoxFactory$ToolBoxImpl$$anonfun$compile$2.apply(ToolBoxFactory.scala:421)
      at scala.tools.reflect.ToolBoxFactory$ToolBoxImpl$withCompilerApi$.liftedTree2$1(ToolBoxFactory.scala:354)
      at scala.tools.reflect.ToolBoxFactory$ToolBoxImpl$withCompilerApi$.apply(ToolBoxFactory.scala:354)
      at scala.tools.reflect.ToolBoxFactory$ToolBoxImpl.compile(ToolBoxFactory.scala:421)
      at scala.tools.reflect.ToolBoxFactory$ToolBoxImpl.eval(ToolBoxFactory.scala:443)
      ... 37 elided
    

    所以,如果您决定要选择哪种类型:

    scala> val x: Base = new A
    x: Base = A@2647e550
    
    scala> tb.eval(tb.parse(s"$imps ; ShowA show x"))
    res4: Any = Base
    
    scala> tb.eval(tb.parse(s"$imps ; ShowA show (x.asInstanceOf[A])"))
    res5: Any = A
    

    【讨论】:

    • 很好,谢谢!但是我必须为Base 的每个可能的子类写一个case。具体来说,它不适用于foreach 循环中的一个通用case,就像在原始帖子中一样。这是为什么呢?
    • @mucaho 为什么?见答案顶部。但是人们确实尝试在静态类型中进行动态解析,有时使用自定义方法来选择要发送到的内容。另一个想法是用工具箱编译你想要的东西。我会更新...
    • 谢谢。我选择这个作为公认的答案,因为它展示了一种动态重载解决方法。
    【解决方案2】:

    这不是您问题的答案,看起来更像是一种解决方法:

      abstract class Base {}
      class A extends Base {}
      class B extends Base {}
    
      trait Show[T] {
        def show(obj: T): String
      }
    
      class ShowBase extends Show[Base] {
        override def show(obj: Base): String = "Base"
      }
      object ShowA extends Show[A] {
        override def show(obj: A): String = "A"
      }
      object ShowB extends Show[B] {
        override def show(obj: B): String = "B"
      }
    
      case class ^[T <: Base](obj: T, show: Show[T])
    
      Seq(^(new A(), ShowA), ^(new B(), ShowB)).foreach {
        case ^(obj, showImpl) => println(showImpl.show(obj), obj.getClass.getSimpleName)
      }
    

    【讨论】:

    • 谢谢,这对原始问题非常有效。现在,如果我可以让它与两个单独的序列一起工作(一个包含对象,另一个包含相应的 show 实现):)
    【解决方案3】:

    我认为重载方法是基于动态绑定调用的,这是一个根本错误(如果您想知道,这种体验就像发现 2+2 是 5 而不是 4)。

    感谢som-snytt's answerblog post about static and dynamic binding in Java,我发现情况并非如此。 基于静态类型调用重载方法。重写的方法是基于动态类型调用的。因此,我原来的问题中的问题是基于静态绑定的:som-snytt's answer 对此进行了更详细的解释。

    【讨论】:

    • 我想让我的生活更轻松,并使用Magnet Pattern 选择在运行时执行的适当方法(而不是求助于som-snitts answer 建议的方法)。但是,它不起作用:磁铁模式使用隐式,但隐式也在编译时解析
    猜你喜欢
    • 1970-01-01
    • 2016-05-02
    • 1970-01-01
    • 1970-01-01
    • 2021-08-24
    • 1970-01-01
    • 1970-01-01
    • 2011-02-14
    • 1970-01-01
    相关资源
    最近更新 更多