您的期望相互矛盾。
您“惊讶”地看到 Scala 在主要版本之间是二进制不兼容的,这表明您期望相反:即使在主要版本之间,Scala 应该也是二进制兼容的。
但与此同时,您希望 Scala 对特征使用编码,该编码依赖于在设计 Scala 2.11 的二进制格式时甚至不存在的特性。 Scala 2.11 的第一个候选版本,即不允许进行更多更改的时间点,是在 Java 8 甚至发布前两周。要求每个 Scala 用户在 Java 8 发布之前就安装它,这是荒谬的。
因此,一方面,您期望完全的二进制兼容性,即完全没有变化。另一方面,您希望使用最新最好的功能,即尽可能快地进行更改。你不能两者兼得。你必须选择。
而且,正如 Alexey 在他的回答中已经指出的那样,正是这样的改进,需要打破二进制兼容性。
如果您具有二进制兼容性,则如果您找到更好的二进制表示,则无法更改您的二进制表示。当目标平台的新功能可用时,您将无法使用它们。这是非常严格的,特别是对于像 Scala 这样的语言,它突破了可以在 JVM 上合理编码的界限。对编译器设计者来说,强迫他们在第一次就“一切正常”是非常苛刻的。
以下是多年来发生变化并破坏向后兼容性的几件事:
- 使用
MethodHandles 的 lambda 编码,当它们被添加到 Java 7 中时。他们不可能“第一次就做到这一点”,因为那时 MethodHandles 甚至不存在。
- (在即将发布的 2.12 中。)lambda 的编码,再次,因此它们与 Java 8 的编码相同。他们不可能“第一次就做对了”,因为当时 Java 中甚至不存在 lambda。
- (在即将发布的 2.12 中。)使用
default 方法在 interfaces 中对特征进行编码。他们不可能“第一次就做对了”,因为当时 Java 中甚至不存在 default 方法。
如果 Java 平台获得正确的尾调用或至少正确的尾递归,我很确定,ABI 将再次更改以利用这些功能。而如果我们在 JVM 中获得了 Value Types,那么 Scala 中 Value Classes 的编码很可能会发生变化。
然而,在dotc, the compiler for Dotty,团队正在尝试一种新的二进制兼容性方法:TASTy。 TASTy 是类型化抽象语法树的序列化格式。这个想法是保证 TASTy 的二进制兼容性,但不保证最终输出。 TASTy 包含了重新编译程序所需的所有信息,所以如果你想组合由不同编译器编译的两个闭源库,这不是问题,因为你可以扔掉编译好的代码并从 TASTy 重新编译。
TASTy 将始终与编译后的代码一起提供。例如。对于 Scala-JVM,序列化的 TASTy 将在 .class 文件或 .jar 的元数据部分中提供,对于 Scala.js,在已编译源文件内的注释或二进制数组中,对于 Scala-native 在元数据部分编译后的.dll、.exe、.so、.dylib等等。
回到你关于特质的具体问题:
目前,单个特征被编码为:
- 一个
interface,包含所有特征方法(抽象和具体)的抽象声明
- 一个包含所有traits具体方法的静态方法的静态类,带有一个额外的参数
$this
- 在继承层次结构中混入特征的每个点,特征中所有具体方法的合成转发器方法转发到静态类的静态方法
所以,下面的 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; }
}
如您所见,保留了带有静态方法和转发器的旧设计,它们只是折叠到界面中。 trait 的具体方法现在已作为static 方法移入接口本身,转发器方法并非在每个类中合成,而是定义一次为default 方法,以及静态$init$ 方法(表示trait body) 也被移到了接口中,因此不需要附带的静态类。
大概可以这样简化:
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; }
}
我不确定为什么没有这样做。乍一看,当前的编码可能会给我们一点前向兼容性:您可以使用用新编译器编译的特征和旧编译器编译的类,这些旧类将简单地覆盖它们从与相同的接口。除此之外,转发器方法将尝试调用不再存在的A$class 和B$class 上的静态方法。