模板初始化期间会发生什么

问题描述 投票:1回答:1

Scala official docs,它说:

如果这是特征的模板,则其mixin-evaluation包括对语句序列statsstats的评估。

如果这不是特征的模板,则其评估包括以下步骤。

首先,评估超类构造函数sc。

然后,模板线性化中的所有基类直到由sc表示的模板的超类都进行混合评估。混合评估以线性化中出现的相反顺序发生。

最后,评估语句序列statsstats。

我想知道“mixin-evaluation”和“superclass constructor evaluation”在这里意味着什么?为什么超类构造函数sc与特征mt1mt2mt3等区别对待?

scala traits
1个回答
1
投票

嗯,这是其中一个复杂的事情,除非你已经知道答案是什么,否则我认为没有一个很好的简短回答。我认为简短的回答是,这是因为Scala被编译为JVM字节码,因此必须匹配该目标平台的限制。不幸的是,我认为这个答案并不清楚,所以我的答案很长。

免责声明:(对于未来的读者来说,这是一个可耻的自我推销):如果你觉得这个很长的答案很有用,你也可以看看my another long answer到另一个问题,由Lifu Huang提出类似话题。

免责声明:翻译示例中的Java代码仅用于说明目的。它们受到Scala编译器实际执行的操作的启发,但在许多细节上与“真实的东西”不匹配。此外,这些例子不能保证工作或编译。除非明确提到,否则我将使用(更简单的)代码示例,它们是Scala 2.12 / Java 8转换的简化版本,而不是旧版(和更复杂的)Scala / Java转换。

关于混合的一些理论

Mixin是面向对象设计中的一个想法,它具有一些或多或少封装的逻辑,然而它本身没有意义,所以它被添加到其他一些类中。这在某种意义上类似于multiple inheritance,多重继承实际上是Scala中设计此功能的方式。

如果你想在Scala中使用一些真实的混合示例,这里有一些:

  1. 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集合层次结构中的核心方法共享高级方法的实现。

  1. 用于依赖注入的Cake pattern基于混合,但这次混合逻辑通常根本没有意义。

这里重要的是,在Scala中,您可以混合使用逻辑(方法)和数据(字段)。

关于Java / JVM和多重继承的一些理论

像C ++这样的语言中的“天真”多重继承具有臭名昭着的Diamond problem。为了解决这个原因,Java的原始设计不支持任何逻辑或字段的多重继承。您可以“扩展”一个基类(完全继承其行为),此外,您可以“实现”许多接口,这意味着该类声称拥有来自接口的所有方法,但您不能拥有任何真正的逻辑继承从您的基本界面。 JVM中存在相同的限制。 20年后,Java 8加入了Default Methods。所以现在你可以继承一些方法,但仍然不能继承任何字段。这简化了Scala 2.12中混合的实现,其代价是要求Java 8作为其目标平台。静止接口不能具有(非静态)字段,因此不能具有构造函数。这是超类构造函数sc与特征mt1mt2mt3等不同处理的主要原因之一。

另外值得注意的是,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)
}

所以用你的话说:

  1. 一个基类Base与一些领域
  2. 混合特性TASimple,包含2个字段(一个初始和一个未初始化)和两个方法
  3. 儿童班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):

  1. aValueNIaValueI作为val声明的一部分。那些必须由SimpleChild实施支持他们的一些领域(没有任何技巧)。
  2. aIncrementAndGetNIaIncrementAndGetI方法有一些逻辑。这些方法可以由SimpleChild继承,并将基于aValueNIaValueI方法。
  3. 一个初始化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);
  }
}

注意现在如何将aIncrementAndGetNIaIncrementAndGetI的实现移动到一些静态方法,这些方法将显式$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继承:通过TB1TB2。此外,TB1TB2都定义了一些aValueNI12的初始化,但具有不同的值。首先应该提到的是,ComplicatedChildval中定义的每个TA只有一个字段副本。但是如果你试试这会发生什么:

val cc = new inheritance.ComplicatedChild(42, 12345)
println(cc.aIncrementAndGetNI12())

哪个值(TB1TB2)会获胜?这种行为是否具有决定性?最后一个问题的答案是 - 是的,行为和编辑之间的行为将是确定性的。这是通过所谓的“特征线性化”实现的,这是一个完全不同的主题。简而言之,Scala编译器以一些固定的定义顺序对所有继承的(直接和间接)特征进行排序,以便它表现出一些良好的行为(例如父特征总是在其列表中的子特征之后)。所以回到引用:

混合评估以线性化中出现的相反顺序发生。

这种特性线性化顺序确保

  1. 在调用某些特征的模拟构造函数时,所有“基础”字段已由相应的父(模拟)构造函数初始化。
  2. 模拟构造函数调用的顺序是固定的,因此行为是确定性的。

在这种特殊情况下,线性化顺序为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)
}

请注意TX0BaseClass是如何继承TASimple的。在这种情况下,我希望线性化产生以下顺序SimpleChild> TASimple> Base> TX0> Any。我将该句子解释如下:在这种情况下,SimpleChild的构造函数不会调用TX0的“模拟”构造函数,因为它在Base(= sc)之后的顺序。我认为这种行为的逻辑是显而易见的:从SimpleChild构造函数的角度来看,TX0的“模拟”构造函数应该已经被Base构造函数调用,而且Base可能已经更新了该调用的结果,因此称之为“模拟“第二次TX0的构造函数可能实际上打破了Base

© www.soinside.com 2019 - 2024. All rights reserved.