特征和接口二进制兼容吗?
我很惊讶Scala
在不同版本中是二进制不兼容的事实。 现在,因为在Java 8
我们有默认的方法实现,它与我们提供的trait
几乎相同,是否可以安全地在Java代码中使用特征? 我自己尝试使用它:
trait TestTrait { def method(v : Int) def concrete(v : Int) = println(v) } public class Test implements TestTrait{ // Compile-error. Implement concrete(Int) @Override public void method(int v) { System.out.println(v); } }
但它拒绝编译。 编译器抱怨没有混淆concrete(Int)
。 虽然我在TestTrait
指定了实现。
当Scala 2.11编译器编译特征时,它不会生成具有默认方法的接口,因为生成的代码必须与Java 6一起使用。在Scala 2.12(需要Java 8)中,它会,因此如果您编译Scala代码使用2.12编译器,我希望您能够以这种方式从Java中使用它(至少对于像这样的简单情况)。
请注意,像这样的更改正是使得不同的Scala版本二进制文件不兼容的原因:如果您尝试使用Scala 2.12中使用Scala 2.11编译的特征,它将尝试调用接口的默认方法,而这些方法不存在。
你的期望相互矛盾。
您很惊讶地看到Scala在主要版本之间是二进制不兼容的,这表明您期望相反:即使在主要版本之间,Scala也应该是二进制兼容的。
然而,与此同时,您希望Scala对依赖于Scala 2.11的二进制格式设计时甚至不存在的function的特征使用编码。 Scala 2.11的第一个候选版本,即不再允许更改的点,是Java 8甚至发布前两周。 要求每个Scala用户在甚至发布之前安装Java 8都是荒谬的。
因此,一方面,您期望完全二进制兼容性,即根本没有变化。 另一方面,您希望使用最新和最好的function,即尽可能快地更改。 你不能兼得。 你必须选择。
而且,正如Alexey在他的回答中已经指出的那样, 正是这样的改进需要打破二进制兼容性。
如果您具有二进制兼容性,那么如果您找到更好的二进制表示,则无法更改二进制表示。 当目标平台可用时,您无法使用它们的新function。 这是非常严格的限制,特别是对于像Scala这样的语言,它推动了可以在JVM上合理编码的边界。 编译器设计者强迫他们第一次获得“一切正确”是非常苛刻的。
以下是一些经过多年改变并破坏向后兼容性的事情:
- 使用
MethodHandle
进行lambda的编码,当它们在Java 7中添加时。他们不能“第一次就MethodHandle
做”,因为当时的MethodHandle
甚至都不存在。 - (在即将发布的2.12中。)lambdas的编码, 再次 ,使它们与Java 8的编码相同。 他们不可能“第一次得到这个权利”,因为当时lambdas甚至不存在于Java中。
- (在即将发布的2.12中。)使用
interface
s中的default
方法对traits进行编码。 他们不可能“第一次就这么做”,因为当时在Java中甚至不存在default
方法。
如果Java平台得到正确的尾调用或者至少是正确的尾部递归,我很确定,ABI将再次改变以利用这些function。 如果我们在JVM中获得Value Types,Scala中的Value Classes的编码可能会改变。
然而 ,在dotc
的编译器中 ,团队正在尝试一种新的二进制兼容性方法: TASTy 。 TASTy是Typed抽象语法树的序列化格式。 这个想法是TASTy保证二进制兼容性,但最终输出不保证。 TASTy包含重新编译程序的所有必要信息,因此如果要组合由不同编译器编译的两个闭源库,那不是问题,因为您可以丢弃已编译的代码并从TASTy重新编译。
TASTy将与编译后的代码一起发送。 例如,对于Scala-JVM,序列化的TASTy将在.class
文件的元数据部分或.jar
,用于Scala.js在编译的源文件内的注释或二进制数组中,对于Scala-native,在元数据部分中。已编译的.dll
, .exe
, .so
, .dylib
等。
回到您关于特征的具体问题:
目前,单个特征编码为:
- 包含所有特征方法(抽象和具体)的抽象声明的
interface
- 一个静态类,包含所有traits的具体方法的静态方法,需要额外的参数
$this
- 在混合特征的inheritance层次结构中的每个点,特征中所有具体方法的合成转发器方法转发给静态类的静态方法
那么,以下Scala代码:
trait A { def foo(i: Int) = i + 1 def abstractBar(i: Int): Int } trait B { def baz(i: Int) = i - 1 } class C extends A with B { override def abstractBar(i: Int) = i * i }
将编码如下:
interface A { int foo(int i); int abstractBar(int i); } abstract class A$class { static void $init$(A $this) {} static int foo(A $this, int i) { return i + 1; } } interface B { int baz(int i); } abstract class B$class { static void $init$(B $this) {} static int baz(B $this, int i) { return i - 1; } } class C implements A, B { public C() { A$class.$init$(this); B$class.$init$(this); } @Override public int baz(int i) { return B$class.baz(this, i); } @Override public int foo(int i) { return A$class.foo(this, i); } @Override public int abstractBar(int i) { return i * i; } }
但在针对Java 8的Scala 2.12中,它看起来更像是这样的:
interface A { static void $init$(A $this) {} static int foo$(A $this, int i) { return i + 1; } default int foo(int i) { return A.foo$(this, i); }; int abstractBar(int i); } interface B { static void $init$(B $this) {} static int baz$(B $this, int i) { return i - 1; } default int baz(int i) { return B.baz$(this, i); } } class C implements A, B { public C() { A.$init$(this); B.$init$(this); } @Override public int abstractBar(int i) { return i * i; } }
如您所见,保留了静态方法和转发器的旧设计,它们只是折叠到界面中。 特征的具体方法现在已作为static
方法移入接口本身,转发器方法不是在每个类中合成,而是一次定义为default
方法,静态$init$
方法(表示特征体中的代码)已被移入界面,使伴侣静态类不必要。
它可能会像这样简化:
interface A { static void $init$(A $this) {} default int foo(int i) { return i + 1; }; int abstractBar(int i); } interface B { static void $init$(B $this) {} default int baz(int i) { return i - 1; } } class C implements A, B { public C() { A.$init$(this); B.$init$(this); } @Override public int abstractBar(int i) { return i * i; } }
我不知道为什么没有这样做。 乍一看,当前编码可能会给我们一些前向兼容性:您可以使用由旧编译器编译的新编译器编译的traits,这些旧类将简单地覆盖它们从接口inheritance的default
转发器方法相同的。 除此之外,转发器方法将尝试在A$class
和不再存在的B$class
上调用静态方法。