在Scala official docs,它说:
如果这是特征的模板,则其mixin-evaluation包括对语句序列statsstats的评估。
如果这不是特征的模板,则其评估包括以下步骤。
首先,评估超类构造函数sc。
然后,模板线性化中的所有基类直到由sc表示的模板的超类都进行混合评估。混合评估以线性化中出现的相反顺序发生。
最后,评估语句序列statsstats。
我想知道“mixin-evaluation”和“superclass constructor evaluation”在这里意味着什么?为什么超类构造函数sc
与特征mt1
,mt2
,mt3
等区别对待?
嗯,这是其中一个复杂的事情,除非你已经知道答案是什么,否则我认为没有一个很好的简短回答。我认为简短的回答是,这是因为Scala被编译为JVM字节码,因此必须匹配该目标平台的限制。不幸的是,我认为这个答案并不清楚,所以我的答案很长。
免责声明:(对于未来的读者来说,这是一个可耻的自我推销):如果你觉得这个很长的答案很有用,你也可以看看my another long answer到另一个问题,由Lifu Huang提出类似话题。
免责声明:翻译示例中的Java代码仅用于说明目的。它们受到Scala编译器实际执行的操作的启发,但在许多细节上与“真实的东西”不匹配。此外,这些例子不能保证工作或编译。除非明确提到,否则我将使用(更简单的)代码示例,它们是Scala 2.12 / Java 8转换的简化版本,而不是旧版(和更复杂的)Scala / Java转换。
关于混合的一些理论
Mixin是面向对象设计中的一个想法,它具有一些或多或少封装的逻辑,然而它本身没有意义,所以它被添加到其他一些类中。这在某种意义上类似于multiple inheritance,多重继承实际上是Scala中设计此功能的方式。
如果你想在Scala中使用一些真实的混合示例,这里有一些:
scala.collection.immutable.List
这样的定义,你会看到很多混音sealed abstract class List[+A] extends AbstractSeq[A]
with LinearSeq[A]
with Product // this one is not a mix-in!
with GenericTraversableTemplate[A, List]
with LinearSeqOptimized[A, List[A]] {
在此示例中,mix-ins用于通过深度和广泛Scala集合层次结构中的核心方法共享高级方法的实现。
这里重要的是,在Scala中,您可以混合使用逻辑(方法)和数据(字段)。
关于Java / JVM和多重继承的一些理论
像C ++这样的语言中的“天真”多重继承具有臭名昭着的Diamond problem。为了解决这个原因,Java的原始设计不支持任何逻辑或字段的多重继承。您可以“扩展”一个基类(完全继承其行为),此外,您可以“实现”许多接口,这意味着该类声称拥有来自接口的所有方法,但您不能拥有任何真正的逻辑继承从您的基本界面。 JVM中存在相同的限制。 20年后,Java 8加入了Default Methods。所以现在你可以继承一些方法,但仍然不能继承任何字段。这简化了Scala 2.12中混合的实现,其代价是要求Java 8作为其目标平台。静止接口不能具有(非静态)字段,因此不能具有构造函数。这是超类构造函数sc
与特征mt1
,mt2
,mt3
等不同处理的主要原因之一。
另外值得注意的是,Java被设计为一种非常安全的语言。特别是它可以解决“未定义的行为”,如果你重新使用一些只是内存中剩余(垃圾)的值,可能会发生这种行为。因此,Java确保在调用其构造函数之前不能访问基类的任何字段。这使得super
在任何子构造函数中调用几乎是强制性的第一行。
Scala和混合(简单示例)
所以现在想象一下你是Scala语言的设计者,你希望它有混合,但你的目标平台(JVM)不支持它们。你该怎么办?显然,您的编译器应该能够将混合内容转换为JVM支持的内容。以下是对简单(和无意义)示例的粗略近似:
class Base(val baseValue: Int) {
}
trait TASimple {
val aValueNI: AtomicInteger
val aValueI: AtomicInteger = new AtomicInteger(0)
def aIncrementAndGetNI(): Int = aValueNI.incrementAndGet()
def aIncrementAndGetI(): Int = aValueI.incrementAndGet()
}
class SimpleChild(val childValue: Int, baseValue: Int) extends Base(baseValue) with TASimple {
override val aValueNI = new AtomicInteger(5)
}
所以用你的话说:
Base
与一些领域TASimple
,包含2个字段(一个初始和一个未初始化)和两个方法SimpleChild
由于TASimple
不仅仅是一个方法声明,因此无法将其编译为简单的Java接口。它实际上编译成这样的东西(在Java代码中):
public abstract interface TASimple
{
abstract void TASimple_setter_aValueI(AtomicInteger param);
abstract AtomicInteger aValueNI();
abstract AtomicInteger aValueI();
default int aIncrementAndGetNI() { return aValueNI().incrementAndGet(); }
default int aIncrementAndGetI() { return aValueI().incrementAndGet(); }
public static void init(TASimple $this)
{
$this.TASimple_setter_aValueI(new AtomicInteger(0));
}
}
public class SimpleChild extends Base implements TASimple
{
private final int childValue;
private final AtomicInteger aValueNI;
private final AtomicInteger aValueI;
public AtomicInteger aValueI() { return this.aValueI; }
public void TASimple_setter_aValueI(AtomicInteger param) { this.aValueI = param; }
public int childValue() { return this.childValue; }
public AtomicInteger aValueNI() { return this.aValueNI; }
public SimpleChild(int childValue, int baseValue)
{
super(baseValue);
TASimple.init(this);
this.aValueNI = new AtomicInteger(5);
}
}
那么TASimple
包含什么以及它是如何被翻译的(到Java 8):
aValueNI
和aValueI
作为val
声明的一部分。那些必须由SimpleChild
实施支持他们的一些领域(没有任何技巧)。aIncrementAndGetNI
和aIncrementAndGetI
方法有一些逻辑。这些方法可以由SimpleChild
继承,并将基于aValueNI
和aValueI
方法。aValueI
的逻辑。如果TASimple
是一个类,它将有一个构造函数,这个逻辑可能就在那里。然而,TASimple
被翻译成一个界面。因此,“构造函数”逻辑被移动到static void init(TASimple $this)
方法,并且从init
构造函数调用SimpleChild
。请注意,Java规范强制要求在它之前调用super
调用(即基类的构造函数)。第3项中的逻辑就是背后的原因
首先,评估超类构造函数sc。 然后,模板线性化中的所有基类直到由sc表示的模板的超类都进行混合评估
这也是JVM本身的逻辑强制执行:首先必须调用基础构造函数,然后才能(并且应该)调用所有混合的所有其他模拟“构造函数”。
旁注(Scala pre-2.12 / Java pre-8)
在Java 8和默认方法之前,翻译会更加复杂。 TASimple
将被翻译成一个界面和类,如
public abstract interface TASimple
{
public abstract void TASimple_setter_aValueI(AtomicInteger param);
public abstract AtomicInteger aValueNI();
public abstract AtomicInteger aValueI();
public abstract int aIncrementAndGetNI();
public abstract int aIncrementAndGetI();
}
public abstract class TASimpleImpl
{
public static int aIncrementAndGetNI(TASimple $this) { return $this.aValueNI().incrementAndGet(); }
public static int aIncrementAndGetI(TASimple $this) { return $this.aValueI().incrementAndGet(); }
public static void init(TASimple $this)
{
$this.TASimple_setter_aValueI(new AtomicInteger(0));
}
}
public class SimpleChild extends Base implements TASimple
{
private final int childValue;
private final AtomicInteger aValueNI;
private final AtomicInteger aValueI;
public AtomicInteger aValueI() { return this.aValueI; }
public void TASimple_setter_aValueI(AtomicInteger param) { this.aValueI = param; }
public int aIncrementAndGetNI() { return TASimpleImpl.aIncrementAndGetNI(this); }
public int aIncrementAndGetI() { return TASimpleImpl.aIncrementAndGetI(this); }
public int childValue() { return this.childValue; }
public AtomicInteger aValueNI() { return this.aValueNI; }
public SimpleChild(int childValue, int baseValue)
{
super(baseValue);
TASimpleImpl.init(this);
this.aValueNI = new AtomicInteger(5);
}
}
注意现在如何将aIncrementAndGetNI
和aIncrementAndGetI
的实现移动到一些静态方法,这些方法将显式$this
作为参数。
Scala和混合#2(复杂的例子)
上一节中的示例说明了一些想法,但并非所有想法。为了更详细的说明,需要更复杂的示例。
混合评估以线性化中出现的相反顺序发生。
当你有几个混合,特别是在diamond problem的情况下,这部分是相关的。考虑以下示例:
trait TA {
val aValueNI0: AtomicInteger
val aValueNI1: AtomicInteger
val aValueNI2: AtomicInteger
val aValueNI12: AtomicInteger
val aValueI: AtomicInteger = new AtomicInteger(0)
def aIncrementAndGetNI0(): Int = aValueNI0.incrementAndGet()
def aIncrementAndGetNI1(): Int = aValueNI1.incrementAndGet()
def aIncrementAndGetNI2(): Int = aValueNI2.incrementAndGet()
def aIncrementAndGetNI12(): Int = aValueNI12.incrementAndGet()
def aIncrementAndGetI(): Int = aValueI.incrementAndGet()
}
trait TB1 extends TA {
val b1ValueNI: AtomicInteger
val b1ValueI: AtomicInteger = new AtomicInteger(1)
override val aValueNI1: AtomicInteger = new AtomicInteger(11)
override val aValueNI12: AtomicInteger = new AtomicInteger(111)
def b1IncrementAndGetNI(): Int = b1ValueNI.incrementAndGet()
def b1IncrementAndGetI(): Int = b1ValueI.incrementAndGet()
}
trait TB2 extends TA {
val b2ValueNI: AtomicInteger
val b2ValueI: AtomicInteger = new AtomicInteger(2)
override val aValueNI2: AtomicInteger = new AtomicInteger(22)
override val aValueNI12: AtomicInteger = new AtomicInteger(222)
def b2IncrementAndGetNI(): Int = b2ValueNI.incrementAndGet()
def b2IncrementAndGetI(): Int = b2ValueI.incrementAndGet()
}
class Base(val baseValue: Int) {
}
class ComplicatedChild(val childValue: Int, baseValue: Int) extends Base(baseValue) with TB1 with TB2 {
override val aValueNI0 = new AtomicInteger(5)
override val b1ValueNI = new AtomicInteger(6)
override val b2ValueNI = new AtomicInteger(7)
}
有趣的是,ComplicatedChild
以两种方式从TA
继承:通过TB1
和TB2
。此外,TB1
和TB2
都定义了一些aValueNI12
的初始化,但具有不同的值。首先应该提到的是,ComplicatedChild
在val
中定义的每个TA
只有一个字段副本。但是如果你试试这会发生什么:
val cc = new inheritance.ComplicatedChild(42, 12345)
println(cc.aIncrementAndGetNI12())
哪个值(TB1
或TB2
)会获胜?这种行为是否具有决定性?最后一个问题的答案是 - 是的,行为和编辑之间的行为将是确定性的。这是通过所谓的“特征线性化”实现的,这是一个完全不同的主题。简而言之,Scala编译器以一些固定的定义顺序对所有继承的(直接和间接)特征进行排序,以便它表现出一些良好的行为(例如父特征总是在其列表中的子特征之后)。所以回到引用:
混合评估以线性化中出现的相反顺序发生。
这种特性线性化顺序确保
在这种特殊情况下,线性化顺序为ComplicatedChild
> TB2
> TB1
> TA
> Base
。这意味着ComplicatedChild
构造函数实际上被翻译成类似于:
public ComplicatedChild(int childValue, int baseValue)
{
super(baseValue);
TA.init(this);
TB1.init(this);
TB2.init(this);
this.aValueNI0 = new AtomicInteger(5);
this.b1ValueNI = new AtomicInteger(6);
this.b2ValueNI = new AtomicInteger(7);
}
所以aValueNI12
将由TB2
初始化(它将覆盖由TB1
“构造函数”设置的值)。
希望这能澄清一下发生了什么以及为什么。如果有什么不清楚,请告诉我。
更新(回答评论)
规范说
然后,模板线性化中直到由scsc表示的模板超类的所有基类都进行混合评估。混合评估以线性化中出现的相反顺序发生。
“达人”在这里究竟意味着什么?
让我们扩展“简单”示例,添加一个更多的基础trait
如下:
trait TX0 {
val xValueI: AtomicInteger = new AtomicInteger(-1)
}
class Base(val baseValue: Int) extends TX0 {
}
trait TASimple extends TX0 {
val aValueNI: AtomicInteger
val aValueI: AtomicInteger = new AtomicInteger(0)
def aIncrementAndGetNI(): Int = aValueNI.incrementAndGet()
def aIncrementAndGetI(): Int = aValueI.incrementAndGet()
}
class SimpleChild(val childValue: Int, baseValue: Int) extends Base(baseValue) with TASimple {
override val aValueNI = new AtomicInteger(5)
}
请注意TX0
和BaseClass
是如何继承TASimple
的。在这种情况下,我希望线性化产生以下顺序SimpleChild
> TASimple
> Base
> TX0
> Any
。我将该句子解释如下:在这种情况下,SimpleChild
的构造函数不会调用TX0
的“模拟”构造函数,因为它在Base
(= sc
)之后的顺序。我认为这种行为的逻辑是显而易见的:从SimpleChild
构造函数的角度来看,TX0
的“模拟”构造函数应该已经被Base
构造函数调用,而且Base
可能已经更新了该调用的结果,因此称之为“模拟“第二次TX0
的构造函数可能实际上打破了Base
。