模板初始化期间会发生什么? [英] What happens during template initialization?

查看:36
本文介绍了模板初始化期间会发生什么?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

Scala 官方文档,它说:

如果这是一个 trait 的模板,那么它的 mixin-evaluation 包括对语句序列 statsstats 的评估.

If this is the template of a trait then its mixin-evaluation consists of an evaluation of the statement sequence statsstats.

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

If this is not a template of a trait, then its evaluation consists of the following steps.

首先,计算超类构造函数 sc.

First, the superclass constructor sc is evaluated.

然后,模板线性化中的所有基类直到模板的由 sc 表示的超类是混合评估的.混合评估在线性化中以相反的顺序发生.

Then, all base classes in the template's linearization up to the template's superclass denoted by sc are mixin-evaluated. Mixin-evaluation happens in reverse order of occurrence in the linearization.

最后,对语句序列 statsstats 进行评估.

Finally, the statement sequence statsstats is evaluated.

我想知道这里的混合评估"和超类构造函数评估"是什么意思?为什么超类构造函数 sc 与特征 mt1mt2mt3 等不同?

I am wondering what the "mixin-evaluation" and "superclass constructor evaluation" mean here? Why is superclass constructor sc treated differently from traits mt1, mt2, mt3, etc.?

推荐答案

嗯,这是复杂的事情之一,我不认为有一个好的简短答案,除非您已经知道答案是什么.我认为简短的回答是,这是因为 Scala 被编译为 JVM 字节码,因此必须匹配该目标平台的限制.不幸的是,我认为这个答案并不明确,所以我的真正答案会很长.

Well, this is one of those complicated things for which I don't think there is a good short answer unless you already know what the answer is. I think the short answer is that this is a result of the fact that Scala is compiled to the JVM bytecode and thus has to match restrictions of that target platform. Unfortunately I don't think this answer is clear so my real answer is going to be long.

免责声明:(对未来读者来说可耻的自我推销):如果你觉得这个很长的答案很有用,你也可以看看 我对另一个问题的另一个长答案Lifu Huang 谈了一个类似的话题.

Disclaimer: (a shameful self-promotion for the future readers): If you find this quite long answer useful, you might also take a look at my another long answer to another question by Lifu Huang on a similar topic.

免责声明:翻译示例中的 Java 代码仅用于说明目的.它们的灵感来自 Scala 编译器的实际功能,但在许多细节上与真实事物"不符.此外,这些示例不能保证工作或编译.除非另有明确说明,否则我将使用(更简单的)代码示例,这些示例是 Scala 2.12/Java 8 翻译的简化版本,而不是较旧(更复杂)的 Scala/Java 翻译.

Disclaimer: Java code in the translation examples is provided solely for illustrative purpose. They are inspired by what the Scala compiler actually does but don't match the "real thing" in many details. Moreover those examples are not guaranteed to work or compile. Unless explicitly mentioned otherwise I will use (simpler) code examples that are simplified versions of Scala 2.12/Java 8 translation rather than older (and more complicated) Scala/Java translations.

一些关于混入的理论

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

Mixin is an idea in an object-oriented design to have some more or less encapsulated piece of logic that however doesn't make sense on its own and so it is added to some other classes. This is in a sense similar to multiple inheritance and multiple inheritance is actually the way this feature is designed in Scala.

如果您想要一些真实世界的 Scala 混合示例,这里有一些:

If you want some real world examples of mix-ins in Scala, here are some:

  1. Scala 集合库实现基于混入.如果您查看诸如 scala.collection.immutable.List 之类的定义,您会看到很多混入
  1. Scala collection library implementation is based on mix-ins. If you look at definition of something like scala.collection.immutable.List you'll see a lot of mix-ins

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

In this example mix-ins are used to share implementation of advanced methods via core methods along the deep and wide Scala collections hierarchy.

  1. 蛋糕模式用于依赖注入是基于混入的,但这次混入逻辑本身通常根本没有意义.
  1. Cake pattern used for dependency injection is based on mix-ins but this time the mixed-in logic typically doesn't make sense on its own at all.

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

What is important here is that in Scala you can mix-in both logic (methods) and data (fields).

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

在像 C++ 这样的语言中完成的朴素"多重继承有一个臭名昭著的钻石问题.为了修复它,Java 的原始设计不支持任何逻辑或字段的多重继承.您可以扩展"恰好一个基类(完全继承其行为),此外您可以实现"许多接口,这意味着该类声称拥有来自接口的所有方法,但您不能继承任何真正的逻辑从您的基本界面.JVM 中也存在同样的限制.20 年后,在 Java 8 中添加了 默认方法.所以现在你可以继承一些方法,但仍然不能继承任何字段.这种在 Scala 2.12 中简化的混入实现,代价是需要 Java 8 作为其目标平台.仍然接口不能有(非静态)字段,因此不能有构造函数.这是超类构造函数 sc 与特征 mt1mt2mt3 被区别对待的主要原因之一> 等

"Naive" multiple inheritance as done in languages like C++ has an infamous Diamond problem. To fix it original design of Java didn't support multiple-inheritance of any logic or fields. You may "extend" exactly one base class (fully inheriting its behavior) and additionally you can "implement" many interfaces which means that the class claims to have all methods from the interface(s) but you can't have any real logic inherited from your base interface. The same restrictions existed in the JVM. 20 years later in Java 8 Default Methods were added. So now you can inherit some methods but still can't inherit any fields. This simplified implementation of mix-ins in Scala 2.12 at the price of requiring Java 8 as its target platform. Still interfaces can't have (non-static) fields and thus can't have constructors. This is one of the major reasons why the superclass constructor sc is treated differently from the traits mt1, mt2, mt3, etc.

另外需要注意的是,Java 被设计为一种非常安全的语言.特别是它与未定义的行为"作斗争,如果您重新使用内存中剩余的一些值(垃圾),可能会发生这种情况.因此,Java 确保在调用其构造函数之前您无法访问基类的任何字段.这使得 super 在任何子构造函数中调用几乎是强制性的第一行.

Also it is important to note that Java was designed as a pretty safe language. Particularly it fights against "undefined behaviors" that might happen if you re-use some values that are just leftover (garbage) in the memory. So Java ensures that you can't access any fields of the base class until its constructor is called. This makes super call pretty much mandatory first line in any child constructor.

Scala 和混入(简单示例)

现在假设您是 Scala 语言的设计者,您希望它具有混合功能,但您的目标平台 (JVM) 不支持它们.你该怎么办?显然,您的编译器应该能够将 mix-ins 转换为 JVM 支持的内容.这是一个简单(和废话)示例的粗略近似:

So now imagine you are a designer of the Scala language and you want it to have mix-ins but your target platform (JVM) doesn't support them. What should you do? Obviously your compiler should be able to convert mix-ins into something that JVM supports. And here is a rough approximation of how it is done for a simple (and nonsense) example:

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
  1. a base class Base with some field
  2. a mix-in trait TASimple which contains 2 fields (one initial and one not initialized) and two methods
  3. a child class SimpleChild

由于TASimple 不仅仅是方法的声明,它不能被编译为一个简单的Java 接口.它实际上被编译成这样(在Java代码中):

Since TASimple is more than just a declaration of methods, it can't be compiled to just a simple Java interface. It is actually compiled into something like this (in Java code):

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):

So what TASimple contains and how it is translated (to Java 8):

  1. aValueNIaValueI 作为 val 声明的一部分.这些必须通过 SimpleChild 来实现,并用一些字段支持它们(没有任何技巧).

  1. aValueNI and aValueI as a part of val declarations. Those must be implemented by SimpleChild backing them with some fields (no tricks whatsoever).

aIncrementAndGetNIaIncrementAndGetI 方法与一些逻辑.这些方法可以由 SimpleChild 继承,并将基于 aValueNIaValueI 方法工作.

aIncrementAndGetNI and aIncrementAndGetI methods with some logic. Those methods can be inherited by SimpleChild and will work basing on the aValueNI and aValueI methods.

初始化aValueI 的一段逻辑.如果 TASimple 是一个类,它会有一个构造函数,并且这个逻辑可能已经存在.但是 TASimple 被转换为接口.因此,构造函数"部分的逻辑被移动到 static void init(TASimple $this) 方法,并且从 SimpleChild 调用 init构造函数.请注意,Java 规范强制要求 super 调用(即基类的构造函数)必须在它之前调用.

A piece of logic that initializes aValueI. If TASimple was a class, it would have a constructor and this logic might have been there. However TASimple is translated to an interface. Thus that "constructor" piece of logic is moved to a static void init(TASimple $this) method and that init is called from the SimpleChild constructor. Note that Java spec enforces that the super call (i.e. the constructor of the base class) must be called before it.

项目#3 中的逻辑是背后的原因

The logic in the item #3 is what stands behind

首先,计算超类构造函数 sc.
然后,模板线性化中的所有基类直到由 sc 表示的模板超类都被混合评估

First, the superclass constructor sc is evaluated.
Then, all base classes in the template's linearization up to the template's superclass denoted by sc are mixin-evaluated

这也是 JVM 本身强制执行的逻辑:首先必须调用基本构造函数,然后才能(并且应该)调用所有混合的所有其他模拟构造函数".

Again this is the logic enforce by the JVM itself: you first have to call the base constructor and only then you can (and should) call all the other simulated "constructors" of all mix-ins.

旁注(Scala 2.12 之前/Java 8 之前)

Side note (Scala pre-2.12/Java pre-8)

在 Java 8 和默认方法之前,翻译会更加复杂.TASimple 将被翻译成接口和类,例如

Before Java 8 and default methods translation would be even more complicated. TASimple would be translated into an interface and class such as

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 作为参数的静态方法的.

Note how now implementations of aIncrementAndGetNI and aIncrementAndGetI are moved to some static methods that take explicit $this as a parameter.

Scala 和 mix-ins #2(复杂示例)

上一节中的示例说明了一些想法,但不是全部.为了更详细的说明,需要一个更复杂的例子.

Example in the previous section illustrated some of the ideas but not all of them. For a more detailed illustration a more complicated example is required.

混合评估在线性化中以相反的顺序发生.

Mixin-evaluation happens in reverse order of occurrence in the linearization.

当您有多个混入时,尤其是在 钻石的情况下,这部分是相关的问题.考虑以下示例:

This part is relevant when you have several mix-ins and especially in the case of the diamond problem. Consider following example:

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 的初始化,但具有不同的值.首先应该提到的是,对于TA 中定义的每个valComplicatedChild 将只有一个字段副本.但是如果你尝试这个会发生什么:

What is interesting here is that ComplicatedChild inherits from TA in two ways: via TB1 and TB2. Moreover both TB1 and TB2 define some initialization of aValueNI12 but with different values. First of all it should be mentioned that ComplicatedChild will have only one copy of fields for each val defined in TA. But then what would happen if you try this:

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

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

Which value (TB1 or TB2) would win? And will the behavior be deterministic at all? The answer to the last question is - yes, the behavior will be deterministic both between runs and between compilations. This is achieved via so called "traits linearization" which is an entirely different topic. In short the Scala compiler sorts all the inherited (directly and indirectly) traits in some fixed defined order such that it manifests some good behaviors (such as the parent trait is always after its child trait in the list). So going back to the quote:

混合评估在线性化中以相反的顺序发生.

Mixin-evaluation happens in reverse order of occurrence in the linearization.

这个特征线性化顺序确保

This traits linearization order ensures

  1. 在调用某些特征的模拟构造函数时,所有基本"字段都已由相应的父(模拟)构造函数初始化.

  1. That all "base" fields are already initialized by the corresponding parent (simulated) constructors by the time the simulated constructor for some trait is called.

模拟构造函数调用的顺序是固定的,因此行为是确定性的.

Order of the simulated constructors calls is fixed so behavior is deterministic.

在这种特殊情况下,线性化顺序将是 ComplicatedChild > TB2 > TB1 > TA > 基础.这意味着 ComplicatedChild 构造函数实际上被翻译成类似:

In this particular case the linearization order will be ComplicatedChild > TB2 > TB1 > TA > Base. It means that ComplicatedChild constructor is actually translated into something like:

  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 构造函数"设置的值).

and so aValueNI12 will be initialized by TB2 (which will overwrite the value set by the TB1 "constructor").

希望这能澄清正在发生的事情以及原因.如果有不清楚的地方,请告诉我.

Hope this clarifies a bit what's going on and why. Let me know if something is not clear.

更新(回复评论)

规范说

然后,模板线性化中的所有基类直到由 scsc 表示的模板超类都被混合评估.Mixin-evaluation 在线性化中以相反的顺序发生.

Then, all base classes in the template's linearization up to the template's superclass denoted by scsc are mixin-evaluated. Mixin-evaluation happens in reverse order of occurrence in the linearization.

高达"在这里的确切含义是什么?

what does the "up to" precisely mean here?

让我们扩展简单"示例,再添加一个基本的 trait,如下所示:

Let's extend the "simple" example adding one more base trait as following:

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 是如何被 BaseClassTASimple 继承的.在这种情况下,我希望线性化产生以下顺序 SimpleChild > TASimple > Base > TX0 > 任何.我将这句话解释如下:在这种情况下,SimpleChild 的构造函数不会调用 TX0 的模拟"构造函数,因为它在 之后的顺序出现>Base (= sc).我认为这种行为的逻辑很明显:从 SimpleChild 构造函数的角度来看,TX0 的模拟"构造函数应该已经被 Base 构造函数,此外 Base 可能已经更新了该调用的结果,因此第二次调用 TX0 的模拟"构造函数实际上可能会破坏 基础.

Note how here TX0 is inherited by both BaseClass and TASimple. In this case I expect linearization to produce the following order SimpleChild > TASimple > Base > TX0 > Any. I interpret that sentence as following: in this case the constructor of the SimpleChild will not call the "simulated" constructor of the TX0 because it comes in the order after the Base (= sc). I think the logic for this behavior is obvious: from the point of view of the SimpleChild constructor the "simulated" constructor of the TX0 should have already been called by the Base constructor, moreover Base might have updated results of that call so calling the "simulated" constructor of the TX0 second time might actually break the Base.

这篇关于模板初始化期间会发生什么?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆