为什么父表中的第一个碱基 class 必须是非性状 class?

Why the first base class in parent list must be non-trait class?

Scala spec 中,据说在 class 模板中 sc extends mt1, mt2, ..., mtn

Each trait reference mti must denote a trait. By contrast, the superclass constructor sc normally refers to a class which is not a trait. It is possible to write a list of parents that starts with a trait reference, e.g. mt1 with …… with mtn. In that case the list of parents is implicitly extended to include the supertype of mt1 as first parent type. The new supertype must have at least one constructor that does not take parameters. In the following, we will always assume that this implicit extension has been performed, so that the first parent class of a template is a regular superclass constructor, not a trait reference.

如果我没有理解错的话,我认为是:

trait Base1 {}
trait Base2 {}
class Sub extends Base1 with Base2 {}

将隐式扩展为:

trait Base1 {}
trait Base2 {}
class Sub extends Object with Base1 with Base2 {}

我的问题是:

  1. 我的理解正确吗?
  2. 是否有此要求(父列表中的第一个子class必须是非特征class)并且隐式扩展仅适用于class模板(例如class Sub extends Mt1, Mt2) 或特征模板(例如 trait Sub extends Mt1, Mt2)?
  3. 为什么需要此要求和隐式扩展?

1) 基本上是的,你的理解是正确的。就像在 Java 中一样,每个 class 都继承自 java.lang.Object(Scala 中的 AnyRef)。因此,由于您正在定义具体的 class,因此您将隐式继承自 Object。如果你查看 REPL,你会得到:

scala> trait Base1 {}
defined trait Base1

scala> trait Base2 {}
defined trait Base2

scala> class Sub extends Base1 with Base2 {}
defined class Sub

scala> classOf[Sub].getSuperclass
res0: Class[_ >: Sub] = class java.lang.Object

2) 是的,从规范中的 "Traits" 段落来看,这也适用于它们。在 "Templates" 段中,我们有:

The new supertype must have at least one constructor that does not take parameters

然后在 "Traits" 段中:

Unlike normal classes, traits cannot have constructor parameters. Furthermore, no constructor arguments are passed to the superclass of the trait. This is not necessary as traits are initialized after the superclass is initialized.

Assume a trait D defines some aspect of an instance x of type C (i.e. D is a base class of C). Then the actual supertype of D in x is the compound type consisting of all the base classes in L(C) that succeed D.

这是定义无参数的基本构造函数所必需的。

3) 根据答案(2),需要定义基础构造函数

免责声明:我不是,也从来不是 "Scala design committee" 或类似组织的成员,所以 "why?" 问题的答案是主要是猜测,但我认为有用。

免责声明 #2:我写这篇文章 post 花了好几个小时和好几次,所以它可能不是很一致

免责声明#3(对未来的读者来说是可耻的self-promotion):如果你觉得这个很长的答案有用,你也可以看看 到 Lifu Huang 关于类似主题的另一个问题。

简答

这是我认为没有好的简短答案的复杂问题之一,除非您已经知道答案是什么。虽然我的真实答案会很长,但以下是我最好的简短答案:

Why the first base class in parent list must be non-trait class?

因为必须只有一个 non-trait 基础 class 如果它总是第一个

会让事情变得更容易
  1. Is my understanding correct?

是的,您的隐含示例就是将要发生的事情。但是我不确定它是否显示了对该主题的完全理解。

  1. Does this requirement (the first subclass in the parent list must be non-trait class) and the implicit extension only applies to class template (e.g. class Sub extends Mt1, Mt2) or also trait template (e.g. trait Sub extends Mt1, Mt2)?

不,特征也会发生隐式扩展。实际上,您还能如何期望 Mt1 有自己的 "supertype" 被提升到扩展它的 class?

实际上这里有两个恕我直言 non-obvious 例子证明这是真的:

示例 #1

trait TAny extends Any

trait TNo 

// works
class CGood(val value: Int) extends AnyVal with TAny 
// fails 
// illegal inheritance; superclass AnyVal is not a subclass of the superclass Object
class CBad(val value: Int) extends AnyVal with TNo 

这个例子失败了,因为规范说

The extends clause extends scsc with mt1mt1 with …… with mtnmtn can be omitted, in which case extends scala.AnyRef is assumed.

所以 TNo 实际上扩展了 AnyRef,这与 AnyVal.

不兼容

示例 #2

class CFirst
class CSecond extends CFirst

// did you know that traits can extend classes as well?
trait TFirst extends CFirst
trait TSecond extends CSecond

// works
class ChildGood extends TSecond with TFirst
// fails 
// illegal inheritance; superclass CFirst is not a subclass of the superclass CSecond of the mixin trait TSecond
class ChildBad extends TFirst with TSecond

再次 ChildBad 失败,因为 TSecond 需要 CSecondTFirst 仅提供 CFirst 作为基础 class。

  1. Why this requirement and the implicit extension is necessary?

主要有以下三个原因:

  1. 与主要目标平台 (JVM) 的兼容性
  2. 特征具有 "mixin" 语义:你有一个 class 并且你在
  3. 中混合了额外的行为
  4. 规范其余部分(例如 linearization rules)的完整性、一致性和简单性。这可能重述如下:每个 class 必须声明 0 或 1 个 base non-trait classes 并且在编译后目标平台强制执行正好有 1 non-trait base class。因此,如果您只是假设总是只有一个碱基 class,那么规范的其余部分就更容易了。以这种方式,当行为取决于基 class.
  5. 时,您只需编写一次此隐式扩展规则,而不是每次都编写

Scala 规范 goals/intentions

我相信当一个人阅读规范时会有两组不同的问题:

  1. 具体写的是什么?规范的含义是什么?
  2. 为什么这么写?意图是什么?

实际上我认为在很多情况下#2 比#1 更重要,但不幸的是规范很少明确包含对该领域的见解。无论如何,我将从我对 #2 的推测开始:Scala 中 classes 系统的 intentions/goals/limitations 是什么? high-level 的主要目标是创建一个比 Java 或 .Net(它们非常相似)中的类型系统更丰富的类型系统,但可以是:

  1. 在那些目标平台上编译回高效代码
  2. 允许 Scala 代码与目标平台中的 "native" 代码进行合理的 two-way 交互

旁注:对 .Net 的支持在几年前就被放弃了,但它多年来一直是目标平台之一,这影响了设计。

单碱基class

简短摘要:本节描述了 Scala 设计人员强烈希望在其中使用 "exactly one base class" 规则的一些原因语言。

OO 设计尤其是继承的一个主要问题是 AFAIK 问题:"where exactly is the border between the "好的和有用的“实践和 "bad" 实践?”开了。这意味着每种语言都必须在使错误成为不可能和使有用成为可能(和容易)之间找到自己的权衡。许多人认为,在 C++ 中,这显然是 Java 和 .Net 的主要灵感来源,但这种权衡过度转移到 "allow everything even if it is potentially harmful" 区域。它使许多新语言的设计者寻求更严格的权衡。特别是 JVM 和 .Net 平台都强制执行将所有类型拆分为 "value types"(又名原始类型)、"classes" 和 "interfaces" 以及每个 class 的规则,除了根 class (java.lang.Object/System.Object),只有一个 "base class" 和零个或多个 "base interfaces"。这个决定是对 multiple inheritance 的许多问题的反应,包括臭名昭著的 "diamond problem" 但实际上还有许多其他问题。

Sidenote(关于内存布局):多重继承的另一个主要问题是object在内存中的布局。考虑以下受 Achilles and the tortoise:

启发的荒谬(在当前 Scala 中是不可能的)示例
trait Achilles {
  def getAchillesPos: Int
  def stepAchilles(): Unit
}

class AchillesImpl(var achillesPos: Int) extends Achilles {
  def getAchillesPos: Int = achillesPos
  def stepAchilles(): Unit = {
    achillesPos += 2
  }
}

class TortoiseImpl(var tortoisePos: Int) {      
  def getTortoisePos: Int = tortoisePos
  def stepTortoise(): Unit = {
    tortoisePos += 1
  }
}

class AchillesAndTortoise(handicap: Int) extends AchillesImpl(0) with TortoiseImpl(handicap) {
  def catchTortoise(): Int = {
    var time = 0
    while (getAchillesPos < getTortoisePos) {
      time += 1
      stepAchilles()
      stepTortoise()
    }
    time 
  }
}

这里棘手的部分是如何实际放置 achillesPostortoisePos 字段ut 在内存中(object)。问题是您可能只想在内存中拥有所有方法的一个编译副本,并且您希望代码高效。这意味着 getAchillesPosstepAchilles 应该知道 achillesPos 相对于 this 指针的一些固定偏移量。同样,getTortoisePosstepTortoise 应该知道 tortoisePos 相对于 this 指针的一些固定偏移量。为了实现这个目标,你必须做出的所有选择看起来都不太好。例如:

  1. 您可能会决定 achillesPos 总是第一个,tortoisePos 总是第二个。但这意味着在 TortoiseImpl 的实例中 tortoisePos 也应该是第二个字段,但是没有任何东西可以填充第一个字段,所以你浪费了一些内存。此外,如果 AchillesImplTortoiseImpl 都来自 pre-compiled 库,您也应该有一些方法来移动对其中字段的访问。

  2. 当您调用 TortoiseImpl 时,您可能会尝试 "fix" this 指针 on-the-fly (AFAIK 这是 C++ 真正工作的方式)。当 TortoiseImpl 是一个抽象 class 时,这变得特别有趣,它通过 extends 知道 trait Achilles(但不是特定的 class AchillesImpl)并试图回调那里的一些方法通过 this 或将 this 传递给以 Achilles 作为参数的某些方法,因此 this 必须是 "fixed back"。请注意,这与 "diamond problem" 不同,因为所有字段和实现只有一个副本。

  3. 您可能同意为了解特定布局的每个特定 class 编译方法的唯一副本。这对内存使用和性能不利,因为它会破坏 CPU 缓存并强制 JIT 对每个缓存进行独立优化。

  4. 你可能会说除了 getter 和 setter 之外没有任何方法可以直接访问这些字段,应该使用 getters 和 setters 代替。或者将所有字段存储在某种实际上相同的字典中。这可能对性能不利(但这是最接近 Scala 使用 mixin-traits 所做的事情)。

在实际的 Scala 中不存在这个问题,因为 trait 不能真正声明任何字段。当您在特征中声明 valvar 时,您实际上声明了一个 getter(和一个 setter)方法,该方法将由特定的 class 扩展了特征,每个 class 都可以完全控制字段的布局。实际上,就性能而言,这很可能会正常工作,因为 JVM (JIT) 可以在许多 real-world 场景中内联这样的虚拟调用。

旁注结束

另一个重点是与目标平台的互操作性。即使 Scala 以某种方式支持 true multiple-inheritance,所以你可以拥有一个继承自 String with Date 的类型,并且可以将其传递给期望 String 和期望 Date 的两种方法,如何从 Java 的角度来看,这看起来像什么?此外,如果目标平台强制执行每个 class 必须是同一根 class (Object) 的(间接)sub-type 的规则,则您无法执行此操作用你的高级语言。

特质和Mix-ins

许多人认为 "one class and many interfaces" trade-off 是在 Java 和 .Net 中制定的,限制太多。例如,它很难在不同的 classes 之间共享某些接口方法的通用默认实现。实际上,随着时间的推移 Java 和 .Net 设计人员似乎得出了相同的结论,并推出了他们自己针对此类问题的修复程序:Extension methods in .Net and then Default methods in Java. Scala designers added a feature called Mixins 这在许多实际案例中都表现良好。然而,与许多其他具有类似功能的动态语言不同,Scala 仍然必须满足 "exactly one base class" 规则和目标平台的其他限制。

重要的是要注意,在实践中使用 mixins 的重要场景是实现 Decorator or Adapter 模式的变体,这两种模式都依赖于您可以将基类型限制为某些东西这一事实比 AnyAnyRef 更具体。这种用法的主要例子是 scala.collection 包。

Scala 语法

所以现在你有以下 goals/restrictions:

  1. 恰好一个碱基 class 每个 class
  2. 能够从 mixin
  3. 向 classes 添加逻辑
  4. 支持具有受限基类型的混合
  5. 类 来自目标平台 (Java) 当从 Scala 看到时被映射到 Scala classes(因为它们还能被映射到什么?)并且它们来了pre-compiled 我们不想打扰他们的实施
  6. 其他优点,例如简单性、类型安全、确定性等

如果你想在你的语言中支持某种多重继承,你需要制定冲突解决规则:当几个基本类型提供了一些适合你的[=337]中的相同"slot"的逻辑时会发生什么=].在禁止特征字段后,我们剩下以下内容g "slots":

  1. 基于目标平台class
  2. 构造函数
  3. 具有相同名称和签名的方法

可能的冲突解决策略是:

  1. 禁止(编译失败)
  2. 决定谁胜谁负
  3. 以某种方式链接它们
  4. 通过重命名以某种方式保留所有内容。这在 JVM 中是不可能的。例如在 .Net 中参见 Explicit Interface Implementation

在某种意义上,Scala 使用了所有可用的(即前 3 个)策略,但 high-level 目标是:让我们尽可能多地保留逻辑。

本次讨论最重要的部分是构造函数和方法的冲突解决。

我们希望不同插槽的规则相同,否则不清楚如何实现安全(如果特征 AB 都覆盖方法 foobarfoobar 的解析规则不同, AB 的不变量可能很容易被破坏)。 Scala 的方法是基于class linearization。简而言之,这些是以某种预测方式将基础 classes 的 "flatten" 层次结构转换为简单线性结构的方法,该方法基于 with 链中的左类型的想法 - "base"(继承级别越高)越多。执行此操作后,方法的冲突解决规则变得简单:通过 super 调用遍历基本类型和链行为的列表;如果未调用 super,则停止链接。这产生了人们可以推理的相当可预测的语义。

现在假设您允许 non-trait class 不是第一个。考虑以下示例:

class CBase {
  def getValue = 2
}
trait TFirst extends CBase {
  override def getValue = super.getValue + 1
}
trait TSecond extends CFirst {
  override def getValue = super.getValue * 2
}
class CThird extends CBase with TSecond {
  override def getValue = 100 - super.getValue
}

class Child extends TFirst with TSecond with CThird

TFirst.getValueTSecond.getValue 应该按什么顺序调用?显然 CThird 已经被编译,你不能改变 super 是什么,所以它必须被移动到第一个位置并且里面已经有 TSecond.getValue 调用。但另一方面,这打破了左边的一切都是基础而右边的一切都是 child 的规则。不引入此类混淆的最简单方法是强制执行 non-trait classes 必须先行的规则。

如果您只是通过将 class CThird 替换为扩展它的 trait 来扩展前面的示例,则同样的逻辑适用:

trait TFourth extends CThird
class AnotherChild extends TFirst with TSecond with TFourth

同样,唯一可以扩展的 non-trait class AnotherChildCThird,这又使得冲突解决规则很难推理。

这就是为什么 Scala 制定了一个更简单的规则:任何提供基数 class 的东西都必须来自第一个位置。然后在特征上扩展相同的规则也是有意义的,所以如果第一个位置被某些特征占据 - 它也定义了基数 class.