Scala 编译器如何处理具体的 trait 方法?
Posted
技术标签:
【中文标题】Scala 编译器如何处理具体的 trait 方法?【英文标题】:How does the Scala compiler handle concrete trait methods? 【发布时间】:2011-10-26 19:05:54 【问题描述】:如果我有以下 Scala 类:
abstract class MyOrdered extends Ordered[MyOrdered]
def id: Int
def compare(that : MyOrdered) : Int =
if (that==null) 1 else (id-that.id)
那么我只需要在Scala中定义id方法就可以得到一个具体的类。但是如果我尝试在 Java 中扩展它,编译器会说 Ordered 的所有具体方法都丢失了。那么,这是否意味着 Scala 编译器只是将 Ordered 的具体方法的实现放在具体的 Scala 类中?
这看起来很浪费,因为我可以有几十个具体的类实现 MyOrdered,而且它们都会得到相同代码的副本,而实际上直接将它放在基类 MyOrdered 中就足够了。此外,这使得创建对 Java 友好的 Scala API 变得非常困难。除了使用虚拟方法实现使类具体化之外,还有什么方法可以强制 Scala 编译器将方法定义放在它应该这样做的地方?
更有趣的是在 Scala trait 中声明一个具体的方法 final。在这种情况下,它仍然没有在扩展 trait 的抽象 Scala 类中实现,但它不能在扩展抽象 Scala 类的 Java 类中实现,因为它被标记为 final。这绝对是一个编译器错误。最终的抽象方法毫无意义,即使它们在 JVM 中是合法的,显然。
【问题讨论】:
编译器错误应报告给问题跟踪器,并带有显示问题的最少源代码示例。 是的,但我不会轻易报告错误,所以我希望先让一些人同意我的观点。毕竟我可能弄错了。 我很乐意帮助您验证它,但我不明白您所描述的问题。小型 Scala 代码示例 + 描述预期行为会有很大帮助。随意编辑您的问题。当涉及到扩展具有从 Java 实现的某些方法的特征时,最好的答案是:不要那样做。迟早会出问题的。原因是 Java 没有任何对应的 Scala 可以映射到,因此它执行了各种从 Java 角度来看令人困惑的技巧。 我认为最终特征方法的问题是 Eclipse 的“表示编译器”而不是 Scala 的问题。无论如何,既然“不支持”在 Java 中扩展 trait-with-code,那么如何处理“final”本质上是无关紧要的。 【参考方案1】:Scala 2.9.1.RC1
让我向您介绍我们在 REPL 中的朋友 :javap
,这对于诊断错误很有用。首先,我们定义类,
scala> abstract class MyOrdered extends Ordered[MyOrdered]
| def id: Int
| def compare(that : MyOrdered) : Int =
| if (that==null) 1 else (id-that.id)
|
defined class MyOrdered
然后要求查看JVM字节码,
scala> :javap -v MyOrdered
Compiled from "<console>"
public abstract class MyOrdered extends java.lang.Object implements scala.math.Ordered,scala.ScalaObject
...
** I'm skipping lots of things here: $less, $lessEq, ... **
...
public boolean $greater(java.lang.Object);
Code:
Stack=2, Locals=2, Args_size=2
0: aload_0
1: aload_1
2: invokestatic #19; //Method scala/math/Ordered$class.$greater:(Lscala/math/Ordered;Ljava/lang/Object;)Z
5: ireturn
LineNumberTable:
line 7: 0
...
public abstract int id();
public int compare(MyOrdered);
Code:
Stack=2, Locals=2, Args_size=2
0: aload_1
1: ifnonnull 8
4: iconst_1
5: goto 17
8: aload_0
9: invokevirtual #38; //Method id:()I
12: aload_1
13: invokevirtual #38; //Method id:()I
16: isub
17: ireturn
LineNumberTable:
line 10: 0
...
我们看到,scalac 实际上在MyOrdered
中生成方法,这些方法对应于特征Ordered
中的具体方法。例如,>
方法被转换为$greater
并且基本上只是调用scala/math/Ordered$class.$greater
。如果我们愿意,我们现在可以查找具体特征定义的字节码,
scala> :javap -v scala.math.Ordered$class
Compiled from "Ordered.scala"
public abstract class scala.math.Ordered$class extends java.lang.Object
...
public static boolean $greater(scala.math.Ordered, java.lang.Object);
Code:
Stack=2, Locals=2, Args_size=2
0: aload_0
1: aload_1
2: invokeinterface #12, 2; //InterfaceMethod scala/math/Ordered.compare:(Ljava/lang/Object;)I
7: iconst_0
8: if_icmple 15
11: iconst_1
12: goto 16
15: iconst_0
16: ireturn
LineNumberTable:
line 46: 0
...
最后,让我们检验您的假设,即 MyOrdered
的子类 M
获得所有方法的完整副本
scala> class M extends MyOrdered def id = 2
defined class M
scala> :javap -v M
Compiled from "<console>"
public class M extends MyOrdered implements scala.ScalaObject
....
** No extra methods besides id **
....
没有,这里好像没有代码重复。
总结一下,
Scalac 对带有具体方法的特征有一些神奇的作用,所以不要试图在 Java 中继承它们。抽象类应该没问题。
JVM 本身不支持符号方法名称、Scala 单例对象,也不支持具有具体方法的特征,因此 Scala 编译器需要进行一些翻译,并使用保留符号 $。
李>如果您仍然遇到 Java 互操作问题,希望 :javap
能帮助您诊断具体问题。
【讨论】:
是的,我花了几个小时,但我得出了同样的结论。我认为我遇到的问题与 scalac 如何在类文件中指定泛型参数有关。查看反编译的 concrete scala 类,我看到“$greater”......采用 java.lang.Object,并且只是从定义它们的抽象基类继承而来。这是我们所期望的。但是 javac 将拒绝编译扩展抽象 scala 类的 Java 类,除非它包含 "$greater" ... with MyOrdered 作为参数,尽管它们显然既不需要也没有使用。奇怪...【参考方案2】:注意:latest commit for Scal 2.12.x(2016 年 8 月)可能会稍微优化 compiler/scala/tools/nsc/backend/jvm
:
SD-192 特征超级访问器的更改方案
而不是将 trait 方法体的代码放入静态方法中, 将其保留在默认方法中。 静态方法(需要作为超级调用的目标)现在使用
invokespecial
来准确调用该方法。
【讨论】:
以上是关于Scala 编译器如何处理具体的 trait 方法?的主要内容,如果未能解决你的问题,请参考以下文章