特征和接口二进制兼容吗?
Are traits and interfaces binary compatible?
令我感到惊讶的是 Scala
在不同版本中是 binary incompatible。现在,由于在 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) 它会,所以如果你用 2.12 编译器编译你的 Scala 代码,我希望你应该能够以这种方式从 Java 使用它(至少对于像这样的简单情况)。
请注意,像这样的更改正是导致不同 Scala 版本二进制不兼容的原因:如果您尝试使用 Scala 2.12 中使用 Scala 2.11 编译的特征,它会尝试调用接口的默认方法,而这些方法不存在。
您的期望相互矛盾。
您 "surprised" 发现 Scala 在主要版本之间是二进制不兼容的,这表明您的期望恰恰相反:Scala 应该 甚至是二进制兼容的在主要版本之间。
但与此同时,您希望 Scala 对特性使用一种编码,这种编码依赖于在设计 Scala 2.11 的二进制格式时甚至不存在的特性。 Scala 2.11 的第一个 Release Candidate,即不允许进行更多更改的时间点,是在 Java 8 发布之前的两周。要求每个 Scala 用户在发布之前安装 Java 8,这太荒谬了。
因此,一方面,您期望完全的二进制兼容性,即完全没有变化。另一方面,您希望使用最新最好的功能,即尽可能快地进行更改。你不能两者兼得。你必须选择。
而且,正如 Alexey 在他的回答中已经指出的那样,正是 这样的改进,需要 破坏二进制兼容性。
如果您具有二进制兼容性,那么如果您找到更好的二进制表示形式,则无法更改您的二进制表示形式。当目标平台的新功能可用时,您将无法使用它们。这是非常严格的,特别是对于像 Scala 这样的语言,它突破了可以在 JVM 上合理编码的界限。强制编译器设计者在第一次就获得 "everything right" 是非常苛刻的。
以下是多年来发生的一些变化并破坏了向后兼容性:
- lambdas的编码,使用
MethodHandle
s,当它们被添加到Java7时。他们不能有"gotten this right the first time",因为那时MethodHandle
s甚至不存在。
- (在即将到来的 2.12.)lambdas 的编码,再次,因此它们与 Java 8 的编码相同。他们不可能有 "gotten this right the first time",因为当时 Java.
甚至不存在 lambdas
- (在即将到来的 2.12 中。)在
interface
中使用 default
方法对特征进行编码。他们不可能有 "gotten this right the first time",因为当时 default
方法在 Java. 中甚至不存在
如果 Java 平台获得适当的尾调用或至少适当的尾递归,我敢肯定,ABI 将再次更改以利用这些功能。而如果我们在 JVM 中获取 Value Types,Scala 中 Value 类 的编码很可能会发生变化。
不过,在dotc
, the compiler for Dotty, the team is trying a new approach to binary compatiblity: TASTy。 TASTy 是类型化抽象语法树的序列化格式。这个想法是保证 TASTy 的二进制兼容性,但不保证最终输出。 TASTy 包含重新编译程序所需的所有信息,所以如果你想合并两个由不同编译器编译的闭源库,那不是问题,因为你可以扔掉编译后的代码并从 TASTy 重新编译。
TASTy 将始终与编译后的代码一起发布。例如。对于 Scala-JVM,序列化的 TASTy 将在 .class
文件或 .jar
的元数据部分中传送,对于 Scala.js 在编译源文件内的注释或二进制数组中,对于 Scala -native 在已编译的 .dll
、.exe
、.so
、.dylib
等的元数据部分。
回到你关于特质的具体问题:
目前,单个特征编码为:
- 一个
interface
包含 all 特征方法(抽象和具体)的抽象声明
- 一个静态 class 包含所有特征的具体方法的静态方法,带一个额外的参数
$this
- 在混合特征的继承层次结构中的每个点,特征中所有具体方法的合成转发器方法转发到静态class
的静态方法
因此,以下 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; }
}
但在 Scala 2.12 中目标为 Java 8,它看起来更像这样:
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
方法移入接口本身,转发器方法不是在每个 class 中合成,而是定义一次为 default
方法,静态 $init$
方法(表示 trait 主体中的代码)也已移至界面中,从而无需伴随静态 class。
大概可以这样简化:
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; }
}
我不确定为什么没有这样做。乍一看,当前的编码可能会给我们一些向前兼容性:你可以使用新编译器编译的特征和旧编译器编译的 classes,那些旧的 classes 将简单地覆盖default
它们从具有相同接口的接口继承的转发器方法。除此之外,转发器方法将尝试调用 A$class
和 B$class
上的静态方法,它们不再存在。
令我感到惊讶的是 Scala
在不同版本中是 binary incompatible。现在,由于在 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) 它会,所以如果你用 2.12 编译器编译你的 Scala 代码,我希望你应该能够以这种方式从 Java 使用它(至少对于像这样的简单情况)。
请注意,像这样的更改正是导致不同 Scala 版本二进制不兼容的原因:如果您尝试使用 Scala 2.12 中使用 Scala 2.11 编译的特征,它会尝试调用接口的默认方法,而这些方法不存在。
您的期望相互矛盾。
您 "surprised" 发现 Scala 在主要版本之间是二进制不兼容的,这表明您的期望恰恰相反:Scala 应该 甚至是二进制兼容的在主要版本之间。
但与此同时,您希望 Scala 对特性使用一种编码,这种编码依赖于在设计 Scala 2.11 的二进制格式时甚至不存在的特性。 Scala 2.11 的第一个 Release Candidate,即不允许进行更多更改的时间点,是在 Java 8 发布之前的两周。要求每个 Scala 用户在发布之前安装 Java 8,这太荒谬了。
因此,一方面,您期望完全的二进制兼容性,即完全没有变化。另一方面,您希望使用最新最好的功能,即尽可能快地进行更改。你不能两者兼得。你必须选择。
而且,正如 Alexey 在他的回答中已经指出的那样,正是 这样的改进,需要 破坏二进制兼容性。
如果您具有二进制兼容性,那么如果您找到更好的二进制表示形式,则无法更改您的二进制表示形式。当目标平台的新功能可用时,您将无法使用它们。这是非常严格的,特别是对于像 Scala 这样的语言,它突破了可以在 JVM 上合理编码的界限。强制编译器设计者在第一次就获得 "everything right" 是非常苛刻的。
以下是多年来发生的一些变化并破坏了向后兼容性:
- lambdas的编码,使用
MethodHandle
s,当它们被添加到Java7时。他们不能有"gotten this right the first time",因为那时MethodHandle
s甚至不存在。 - (在即将到来的 2.12.)lambdas 的编码,再次,因此它们与 Java 8 的编码相同。他们不可能有 "gotten this right the first time",因为当时 Java. 甚至不存在 lambdas
- (在即将到来的 2.12 中。)在
interface
中使用default
方法对特征进行编码。他们不可能有 "gotten this right the first time",因为当时default
方法在 Java. 中甚至不存在
如果 Java 平台获得适当的尾调用或至少适当的尾递归,我敢肯定,ABI 将再次更改以利用这些功能。而如果我们在 JVM 中获取 Value Types,Scala 中 Value 类 的编码很可能会发生变化。
不过,在dotc
, the compiler for Dotty, the team is trying a new approach to binary compatiblity: TASTy。 TASTy 是类型化抽象语法树的序列化格式。这个想法是保证 TASTy 的二进制兼容性,但不保证最终输出。 TASTy 包含重新编译程序所需的所有信息,所以如果你想合并两个由不同编译器编译的闭源库,那不是问题,因为你可以扔掉编译后的代码并从 TASTy 重新编译。
TASTy 将始终与编译后的代码一起发布。例如。对于 Scala-JVM,序列化的 TASTy 将在 .class
文件或 .jar
的元数据部分中传送,对于 Scala.js 在编译源文件内的注释或二进制数组中,对于 Scala -native 在已编译的 .dll
、.exe
、.so
、.dylib
等的元数据部分。
回到你关于特质的具体问题:
目前,单个特征编码为:
- 一个
interface
包含 all 特征方法(抽象和具体)的抽象声明 - 一个静态 class 包含所有特征的具体方法的静态方法,带一个额外的参数
$this
- 在混合特征的继承层次结构中的每个点,特征中所有具体方法的合成转发器方法转发到静态class 的静态方法
因此,以下 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; }
}
但在 Scala 2.12 中目标为 Java 8,它看起来更像这样:
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
方法移入接口本身,转发器方法不是在每个 class 中合成,而是定义一次为 default
方法,静态 $init$
方法(表示 trait 主体中的代码)也已移至界面中,从而无需伴随静态 class。
大概可以这样简化:
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; }
}
我不确定为什么没有这样做。乍一看,当前的编码可能会给我们一些向前兼容性:你可以使用新编译器编译的特征和旧编译器编译的 classes,那些旧的 classes 将简单地覆盖default
它们从具有相同接口的接口继承的转发器方法。除此之外,转发器方法将尝试调用 A$class
和 B$class
上的静态方法,它们不再存在。