为什么父级列表中的第一个基类必须是非特征类? [英] Why the first base class in parent list must be non-trait class?

查看:79
本文介绍了为什么父级列表中的第一个基类必须是非特征类?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

Scala中规范,据说是在类模板sc extends mt1, mt2, ..., mtn

每个特征引用mti必须表示一个特征.相比之下, 超类构造函数sc通常是指不是 特征.可以写一个以 特质参考,例如mt1与……和mtn.在这种情况下 父母名单被隐式扩展为包括 mt1作为第一个父类型.新超类型必须至少具有一个 不带参数的构造函数.接下来,我们将 始终假定已执行此隐式扩展,因此 模板的第一个父类是常规超类 构造函数,而不是特征引用.

如果我理解正确,我认为它的意思是:

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 Sub extends Mt1, Mt2)还是特征模板(例如trait Sub extends Mt1, Mt2)吗?
  3. 为什么需要此要求和隐式扩展?

解决方案

免责声明:我不是,也从未是"Scala设计委员会"或类似机构的成员,所以答案是关于为什么?"问题主要是猜测,但我认为是有用的.

免责声明#2 :我已经写了好几个小时,花了好几次时间写这篇文章,所以可能不太一致

免责声明#3 (对将来的读者来说是可耻的自我宣传):如果您觉得这个较长的答案很有用,还可以看看

  • 此要求(父列表中的第一个子类必须是非特征类)并且隐式扩展名仅适用于类模板(例如class Sub extends Mt1, Mt2)还是特征模板(例如trait Sub extends Mt1, Mt2)吗?
  • 不,隐式扩展也发生在特征上.实际上,您还能如何期望Mt1具有自己的超类型",将其提升为扩展它的类?

    实际上,这是两个恕我直言的非显而易见的例子,证明了这是真的:

    示例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 
    

    此示例失败,因为规范说

    可以省略扩展子句extends scsc with mt1mt1 with …… with mtnmtn,在这种情况下,假定为extends scala.AnyRef.

    所以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要求CSecond,但TFirst仅提供CFirst作为基类.

    1. 为什么需要此要求和隐式扩展?

    三个主要原因:

    1. 与主要目标平台(JVM)的兼容性
    2. 特质具有"mixin" 语义:您有一个类,并且在其中混合了其他行为
    3. 其余规范的完整性,一致性和简单性(例如

    附带说明 :. Net的支持在几年前就已放弃,但多年来它一直是目标平台之一,这影响了设计.

    单个基类

    简短摘要 :本节描述了Scala设计师强烈希望在该语言中使用完全一个基类"规则的一些原因.

    面向对象设计尤其是继承的一个主要问题是AFAIK问题:好的和有用的"实践与坏的"实践之间的界限到底在哪里?"开了.这意味着每种语言都必须找出自己的权衡,即在不可能出错的情况下和在(容易)有用的情况之间进行权衡.许多人认为,对于Java和.Net来说,显然是C ++的主要灵感.在C ++中,这种权衡过度地转移到了允许一切,即使它可能有害"的区域.它使许多新语言的设计师寻求更多的限制权衡.特别是JVM和.Net平台都强制执行以下规则:将所有类型分为值类型"(aka基本类型),类"和接口",以及除根类(java.lang.Object/System.Object)之外的每个类恰好具有一个基本类"和零个或多个基本接口".该决定是对许多问题的回应,这些问题多重继承,包括臭名昭著的钻石问题",但实际上很多其他人也是如此.

    Sidenote (关于内存布局):多重继承的另一个主要问题是内存中的对象布局.考虑以下受阿喀琉斯和乌龟:

    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字段.问题是您可能只希望内存中所有方法的一个已编译副本,并且您希望代码高效.这意味着getAchillesPosstepAchilles应该已经知道achillesPos相对于this指针的某个固定偏移量.类似地,getTortoisePosstepTortoise应该相对于this指针知道tortoisePos的一些固定偏移量.而且您要实现此目标所需的所有选择看起来都不是很好.例如:

    1. 您可能会决定achillesPos始终排在第一位,而tortoisePos始终排在第二位.但这意味着在TortoiseImpl tortoisePos的实例中也应该是第二个字段,但是没有任何内容可以填充第一个字段,因此会浪费一些内存.此外,如果AchillesImplTortoiseImpl都来自预编译的库,则还应该有一些方法可以移动对它们中字段的访问.

    2. 当您调用TortoiseImpl时,您可能会尝试即时修复"this"指针(AFAIK,这是C ++真正起作用的方式).当TortoiseImpl是通过extends知道trait Achilles(但不是特定的class AchillesImpl)并尝试通过this从那里回调某些方法或传递某种采用Achilles作为参数的方法,因此必须将"c​​23>"固定回去.请注意,这与钻石问题"不同,因为所有字段和实现只有一个副本.

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

    4. 您可能会说,除了getter和setter之外,没有其他方法可以直接访问字段,而应该使用getter和setters.或者将所有字段存储在某种实际上是相同的字典中.这可能会降低性能(但这与Scala使用mixin-traits最接近).

    在实际的Scala中,此问题不存在,因为trait不能真正声明任何字段.在特征中声明valvar时,实际上是在声明一个getter(和setter)方法,这些方法将由扩展该特征的特定类实现,并且每个类都可以完全控制字段的布局.实际上,就性能而言,这很可能行得通,因为JVM(JIT)可以在许多实际场景中内联这样的虚拟调用.

    旁注的结尾

    另一个要点是与目标平台的互操作性.即使Scala以某种方式支持真正的多重继承,因此您也可以拥有从String with Date继承的类型,并且可以将其传递给期望String和期望Date的两个方法,但从Java角度来看,它的外观如何观点?另外,如果目标平台强制执行以下规则,即每个类都必须是同一根类(Object)的(间接)子类型,则无法使用高级语言解决此问题.

    特质和混搭

    许多人认为用Java和.Net进行的一类多接口"折衷过于严格.例如,这使得很难在不同类之间共享某些接口方法的通用默认实现.实际上,随着时间的流逝,Java和.Net设计人员似乎得出了相同的结论,并推出了针对此类问题的自己的修补程序: Mixins 的功能,该功能在许多实际情况下都很好.但是,与许多其他具有类似功能的动态语言不同,Scala仍必须满足完全一个基类"规则以及目标平台的其他限制.

    需要特别注意的是,在实践中使用混入文件的某些重要情况是实现适配器模式都取决于您可以将您的基本类型限制为比AnyAnyRef更具体的内容.此类用法的主要示例是scala.collection软件包.

    Scala语法

    因此,您现在具有以下目标/限制:

    1. 每个班级只有一个基本班级
    2. 能够从mixins向类添加逻辑
    3. 支持具有受限基本类型的mixins
    4. 从Scala看到时,来自目标平台(Java)的类被映射到Scala类(因为它们还可以映射到其他什么?),并且它们已经预先编译,因此我们不想弄乱它们的实现
    5. 其他优良品质,例如简单性,类型安全性,确定性等.

    如果您希望使用某种语言来支持多种继承,则需要制定冲突解决规则:当几种基本类型提供适合您类中相同槽"的逻辑时会发生什么.禁止特征中的字段后,我们剩下以下槽":

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

    可能的冲突解决策略是:

    1. 禁止(失败编译)
    2. 确定哪一个获胜并抹去其他人
    3. 以某种方式链接它们
    4. 通过重命名以某种方式保留所有内容.在JVM中,这实际上是不可能的.例如,在.Net中,请参见显式接口实施

    从某种意义上说,Scala使用了所有可用的(即前3个)策略,但高级目标是:让我们尝试保留尽可能多的逻辑.

    此讨论中最重要的部分是构造函数和方法的冲突解决.

    我们希望不同插槽的规则是相同的,因为否则不清楚如何实现安全性(如果特征AB都覆盖方法foobar,但是bar是不同的,AB的不变量可能很容易被破坏). Scala的方法基于with链中的左侧类型-基础"越多(在"c62>"中越高).继承).完成此操作后,方法的冲突解决规则将变得很简单:您可以通过super调用浏览基本类型和链行为的列表;如果未调用super,则停止链接.这产生了人们可以推理的非常可预测的语义.

    现在假设您允许非特征类不是第一个.考虑以下示例:

    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调用.但是,另一方面,这违反了以下规则:左侧的所有内容都是基础,而右侧的所有内容都是子级.不会引起这种混乱的最简单方法是强制执行非特征类必须首先进入的规则.

    如果仅通过用扩展其的trait替换class CThird来扩展前面的示例,则适用相同的逻辑:

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

    同样,唯一可以扩展的非特征类AnotherChildCThird,这再次使解决冲突的规则变得很难推理.

    这就是Scala简化规则的原因:提供基类的任何内容都必须来自第一个位置.然后在特征上扩展相同的规则也是有意义的,因此,如果第一个特征被某些特征占据,则它也定义了基类.

    In the Scala spec, it's said that in a class template 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.

    If I understand it correctly, I think it means:

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

    Will be implicitly extended to:

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

    My questions are:

    1. Is my understanding correct?
    2. 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)?
    3. Why this requirement and the implicit extension is necessary?

    解决方案

    Disclaimer: I'm not and never was a member of the "Scala design committee" or anything like that, so the answer on the "why?" question is mostly speculation but I think a useful one.

    Disclaimer #2: I've written this post over several hours and in several takes so it is probably not very consistent

    Disclaimer #3 (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.

    Short answers

    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. Although my real answer will be long, here are my best short answers:

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

    Because there has to be only one non-trait base class and it makes thing easier if it is always the first

    1. Is my understanding correct?

    Yes, your implicit example is what will happen. However I'm not sure that it shows full understanding of the topic.

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

    No, implicit extensions happens for traits as well. Actually how else you could expect Mt1 to have its own "supertype" to be promoted down to the class that extends it?

    Actually here are two IMHO non-obvious examples proving this is true:

    Example #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 
    

    This example fails because the spec says

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

    so TNo actually extends AnyRef which is incompatible with AnyVal.

    Example #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
    

    Again ChildBad fails because TSecond requires CSecond but TFirst only provides CFirst as the base class.

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

    There are three major reasons:

    1. Compatibility with the main target platform (JVM)
    2. Traits have "mixin" semantics: you have a class and you mix additional behavior in
    3. Completeness, consistency and simplicity of the rest of the spec (e.g. of linearization rules). This might be restated as following: each class must declare 0 or 1 base non-trait classes and after compilation the target platform enforces that there will be exactly 1 non-trait base class. So it makes the rest of the spec easier if you just assume there is always exactly one base class. In such way you have to write this implicit extension rules only once rather than each time when the behavior depends on the base class.

    Scala spec goals/intentions

    I believe that when one reads a spec there are two different sets of questions:

    1. What exactly is written? What is the meaning of the spec?
    2. Why it is written so? What was the intention?

    Actually I think in many cases #2 is more important than #1 but unfortunately specs rarely explicitly contain insights into that area. Anyway I will start with my speculations over #2: what were the intentions/goals/limitations of the classes system in Scala? The main high-level goal was to create a type system richer than the one in Java or .Net (which are quite similar) but that can be:

    1. compiled back to an efficient code in those target platforms
    2. allow reasonable two-way interaction between the Scala code and the "native" code in the target platforms

    Side note: Support of the .Net was dropped years ago but it was one of the target platforms for years and this affected the design.

    Single base class

    Short summary: this section describes some reasons why Scala designers had a strong motivation to have the "exactly one base class" rule in the language.

    A major problem with OO design and particularly inheritance is that AFAIK the question: "where exactly is the border between the "good and useful" practices and the "bad" ones?" is open. It means that each language must find out its own trade off between making impossible what is wrong and making possible (and easy) what is useful. Many believe that in C++, which obviously was a major inspiration for Java and .Net, that trade off is shifted too much into "allow everything even if it is potentially harmful" zone. It made many designers of newer languages to seek for more restricting trade off. Particularly both JVM and .Net platform enforce the rule that all types are split into "value types" (aka primitive types), "classes" and "interfaces" and each class, except the root class (java.lang.Object/System.Object), has exactly one "base class" and zero or more "base interfaces". This decision was a reaction to many issues of multiple inheritance including infamous "diamond problem" but actually many others as well.

    Sidenote (about memory layout): Another major problem with multiple inheritance is objects layout in memory. Consider following ridiculous (and impossible in current Scala) example inspired by Achilles and the tortoise:

    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 
      }
    }
    

    The tricky part here is how to actually lay achillesPos and tortoisePos fields out in the memory (of the object). The issue is that you probably want to have only one compiled copy of all the methods in the memory and you want the code to be efficient. This means that getAchillesPos and stepAchilles should have know some fixed offset of the achillesPos regarding to the this pointer. Similarly getTortoisePos and stepTortoise should have know some fixed offset of the tortoisePos regarding to the this pointer. And all choices you have to achieve this goal don't look nice. For example:

    1. You might decide that achillesPos is always first and tortoisePos is always second. But this means that in the instances of TortoiseImpl tortoisePos should also be the second field but there is nothing to fill the first field with so you waste some memory. Moreover if both AchillesImpl and TortoiseImpl come from pre-compiled libraries, you should have some way to move access to the fields in them as well.

    2. You might try to "fix" this pointer on-the-fly when you call into TortoiseImpl (AFAIK this is the way C++ really works). This becomes especially funny when TortoiseImpl is an abstract class that is aware of the trait Achilles (but not the specific class AchillesImpl) via extends and tries to call back some methods from there via this or pass this to some method that takes Achilles as an argument so this has to be "fixed back". Note that this is not the same as the "diamond problem" because there is only one copy of all fields and implementations.

    3. You might agree to have a unique copy of the methods compiled for each specific class that are aware of the specific layout. This is bad for memory usage and performance because it blows CPU caches and forces JIT to make independent optimizations for each.

    4. You might say that no method except for getter and setter can have direct access to the fields and should use getters and setters instead. Or store all the fields in some kind of a dictionary which is effectively the same. This might be bad for performance (but this is the closest to what Scala does with mixin-traits).

    In the actual Scala this issue does not exist because trait can't really declare any fields. When you declare val or var in a trait, you actually declare a getter (and a setter) method(s) that will be implemented by particular class that extends the trait and each class has full control over layout of the fields. And actually in terms of performance this most probably would work OK because JVM (JIT) can inline such a virtual call in many real-world scenarios.

    End of the Sidenote

    Another major point is interoperability with the target platform. Even if Scala somehow supported true multiple-inheritance so you can have a type that inherits from String with Date and that can be passed to both methods that expect String and that expect Date, how this would look like from the Java point of view? Also if the target platform enforces the rule that every class has to be an (indirect) sub-type of the same root class (Object), you can't work this around in your higher level language.

    Traits and Mix-ins

    Many think that "one class and many interfaces" trade-off that was made in Java and .Net is too restrictive. For example it makes it hard to share common default implementation of some of the interface methods between different classes. Actually over the time Java and .Net designers seem to come to the same conclusion and rolled out they own fixes for this kind of issues: Extension methods in .Net and then Default methods in Java. Scala designers added a feature called Mixins that was known to fare well in many practical cases. However unlike many other dynamic languages that has similar feature, Scala still had to meet the "exactly one base class" rule and other limitations of the target platform.

    It is important to note that there are important scenarios when mixins are used in practice is to implement a variation of the Decorator or Adapter patterns both of which relies on the fact that you can restrict your base type to something more specific than Any or AnyRef. Prime example of such usage is the scala.collection package.

    Scala syntax

    So now you have following goals/restrictions:

    1. Exactly one base class for each class
    2. Ability to add logic to classes from mixins
    3. Support of mixins with restricted base type
    4. Classes from the target platform (Java) when seen from Scala are mapped to the Scala classes (because what else they can be mapped to?) and they come pre-compiled and we don't want to mess with their implementation
    5. Other good qualities such as simplicity, type safety, determinism, etc.

    If you want some kind of multiple inheritance support in your language, you need to develop conflict resolution rules: what happens when several base types provide some logic that would fit the same "slot" in your class. After prohibition of fields in traits we are left with the following "slots":

    1. Base class in terms of the target platform
    2. Constructors
    3. Methods with the same name and signature

    And possible conflict resolution strategies are:

    1. Prohibit (fail compilation)
    2. Decide which one wins and wipes others
    3. Somehow chain them
    4. Somehow preserve all with renaming. This is not really possible in JVM. For example in .Net see Explicit Interface Implementation

    In a sense Scala uses all available (i.e. first 3) strategies but the high-level goal is: let's try to preserve as many logic as we can.

    The most important part for this discussion is conflicts resolution for constructors and methods.

    We want the rules to be the same for different slots because otherwise it is not clear how to achieve safety (if traits A and B both override methods foo and bar but resolution rules for foo and bar are different, invariants for A and B might easily be broken). Scala's approach is based on the class linearization. In short these is the way to "flatten" hierarchy of the base classes into a simple linear structure in some predictive way that is based on the idea that the lefter type in the with chain - the more "base" (higher in the inheritance) it is. After you do this, conflict resolution rule for methods becomes simple: you go through the list of the base types and chain behavior via super calls; if super is not called, you stop chaining. This produce quite predictable semantics that people can reason about.

    Now assume you allow non-trait class to be not first. Consider following example:

    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
    

    In which order TFirst.getValue and TSecond.getValue should be called? Obviously CThird is already compiled and you can't change what the super for it is, so it has to be moved to the first position and there is already TSecond.getValue call inside it. But on the other hand this breaks the rule that everything on the left is base and everything on the right is child. The simplest way to not introduce such confusion is to enforce the rule that non-trait classes must go first.

    The same logic applies if you just extend the previous example by substituting class CThird with a trait that extends it:

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

    Again, the only non-trait class AnotherChild can extend is CThird and this again makes conflict resolution rules quite hard to reason about.

    That's why Scala makes a rule much simpler: whatever provides the base class must come from the first position. And then it makes sense to extend the same rule upon the traits as well so if the first position is occupied by some trait - it also defines the base class.

    这篇关于为什么父级列表中的第一个基类必须是非特征类?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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