当前位置: 首页 > 知识库问答 >
问题:

为什么父列表中的第一个基类必须是非trait类?

严誉
2023-03-14

在 Scala 规范中,据说在类模板中,sc 扩展了 mt1、mt2、...、mtn

每个trait引用mti必须表示一个trait。相比之下,超类构造函数sc通常指的不是trait的类。可以编写以trait引用开头的父类列表,例如mt1 with......与mtn。在这种情况下,父类列表被隐式扩展以包括mt1的超类型作为第一个父类型。新的超类型必须至少有一个不带参数的构造函数。在下面,我们将始终假设已经执行了这个隐式扩展,因此模板的第一个父类是常规的超类构造函数,而不是trait引用。

如果我理解正确,我认为这意味着:

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

将隐式扩展到:

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

我的问题是:

    < li >我的理解正确吗? < li >此要求(父列表中的第一个子类必须是非trait类)和隐式扩展仅适用于类模板(例如< code>class Sub extends Mt1,Mt2)还是也适用于trait模板(例如< code>trait Sub extends Mt1,Mt2)? < li >为什么这个要求和隐式扩展是必要的?

共有2个答案

何灼光
2023-03-14

1)基本上是的,你的理解是正确的。与Java中一样,每个类都继承自< code > Java . lang . object (Scala中的< code>AnyRef)。因此,由于您正在定义一个具体的类,您将隐式地从< code>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)是的,从规格中的“特征”段落来看,这也适用于他们。在“模板”段落中,我们有:

新的超类型必须至少有一个不带参数的构造函数

然后在“特征”段落中:

与普通类不同,性状不能有构造函数参数。此外,没有构造函数参数被传递给性状的超类。这是不必要的,因为性状是在超类初始化后初始化的。

假设特征D定义了类型C的实例x的某个方面(即D是C的基类)。那么x中D的实际超类型就是由L(C)中继承D的所有基类组成的复合类型。

这是定义不带参数的基构造函数所必需的。

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

屈升
2023-03-14

免责声明:我不是,也从来不是“Scala设计委员会”或类似组织的成员,所以答案是“为什么?”这个问题主要是猜测,但我认为这是一个有用的问题。

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

免责声明#3(对未来读者来说是可耻的自运营):如果你觉得这个相当长的答案有用,你也可以看看我对黄立夫关于类似话题的另一个问题的另一个长答案。

简短回答

这是那些复杂的事情之一,除非你已经知道答案是什么,否则我认为没有一个好的简短答案。虽然我真正的答案很长,但这是我最好的简短答案:

为什么父列表中的第一个基类必须是非trait类?

因为必须只有一个非特征基类,如果它总是第一个,它会使事情变得更容易

  1. 我的理解正确吗

是的,你隐含的例子就是将要发生的事情。然而,我不确定它是否显示了对该主题的充分理解。

不,隐式扩展也适用于特征。实际上,你还能指望< code>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子句< code > extends SCSC with mt1mt 1 with……with MTN MTN 可以省略,在这种情况下< code >扩展scala。假设AnyRef。

所以TNo实际上扩展了与AnyVal不兼容的AnyRef

实施例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需要cssecond,但TFirst仅提供CFirst作为基类。

有三个主要原因:

    < li >与主目标平台(JVM)的兼容性 < li>Traits有“mixin”语义:你有一个类,你在其中混合了额外的行为 < li >规范其余部分的完整性、一致性和简单性(如线性化规则)。这可以重新表述如下:每个类必须声明0或1个非特征基类,并且在编译之后,目标平台强制要求只有一个非特征基类。因此,如果您只是假设始终只有一个基类,那么它会使规范的其余部分变得更容易。这样,当行为依赖于基类时,你只需要编写一次隐式扩展规则,而不是每次都要编写。

Scala规范目标/意图

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

  1. 到底写了什么?规范是什么意思?
  2. 为什么这样写?意图是什么?

事实上,我认为在很多情况下#2比351更重要,但不幸的是,规范很少明确包含对该领域的见解。无论如何,我将从我对#2的推测开始:Scala中类系统的意图/目标/限制是什么?主要的高级目标是创建一个比Java或.Net中的类型系统更丰富的类型系统(两者非常相似),但可以是:

  1. 编译回目标平台中的高效代码
  2. 允许目标平台中的Scala代码和“本机”代码之间进行合理的双向交互

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

单基类

简短总结:这一部分描述了Scala设计者有强烈动机在语言中使用“恰好一个基类”规则的一些原因。

面向对象设计的一个主要问题,特别是继承,是回避这样一个问题:“好的和有用的”实践和“坏的”实践之间的界限到底在哪里?”是开放的。这意味着每一种语言都必须找到自己的权衡,即让错误变得不可能,让有用的变得可能(并且容易)。许多人认为在C语言中,这显然是Java和。Net中,这种权衡被过多地转移到“允许一切,即使它是潜在有害的”区域。这使得许多新语言的设计者寻求更具限制性的折衷。尤其是JVM和。Net平台执行这样的规则,即所有类型都被分为“值类型”(也称为基本类型)、“类”和“接口”以及每个类,除了根类(< code > Java . lang . object /< code > System)。对象),只有一个“基类”和零个或多个“基接口”。这一决定是对许多多重继承问题的反应,包括臭名昭著的“钻石问题”,但实际上还有许多其他问题。

旁注(关于内存布局):多重继承的另一个主要问题是内存中的对象布局。考虑以下由阿喀琉斯和乌龟启发的荒谬(在当前的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 
  }
}

这里棘手的部分是如何将跟腱Pos陆龟场实际放置在(对象的)内存中。问题是,您可能只想在内存中拥有所有方法的一个已编译副本,并且希望代码高效。这意味着 getAchillesPosstepAchilles 应该知道关于这个指针的 achillesPos 的一些固定偏移。类似地,getTortoisePosstepTortoise应该知道一些关于这个指针的Pos的固定偏移。而你要实现这个目标的所有选择看起来都不好看。例如:

> < li>

您可以决定< code>achillesPos总是第一位,而< code>tortoisePos总是第二位。但这意味着在< code > TortoiseImpl < code > tortoisePos 的实例中,第二个字段也应该是,但第一个字段没有任何内容可以填充,因此会浪费一些内存。此外,如果< code > achille simple 和< code>TortoiseImpl都来自预编译的库,您也应该有一些方法来移动对它们中的字段的访问。

当您调用TortoiseImpl时,您可能会尝试动态“修复”指针(AFAIK这是C真正的工作方式)。当TortoiseImpl是一个抽象类时,这变得特别有趣,它通过扩展并尝试从那里回调一些方法,或者将其传递给一些将阿喀琉斯作为参数的方法,所以这个 必须“修复回来”。请注意,这与“钻石问题”不同,因为所有字段和实现只有一个副本。

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

您可能会说,除了getter和setter之外,没有任何方法可以直接访问字段,因此应该使用getter和setter。或者将所有字段存储在某种实际上相同的字典中。这可能会影响性能(但这最接近Scala对混合特性所做的事情)。

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

旁注结束

另一个要点是与目标平台的互操作性。即使Scala以某种方式支持真正的多重继承,所以你可以拥有一个从< code>String with Date继承的类型,并且可以传递给期望< code>String和期望< code>Date的两个方法,从Java的角度来看,这是什么样子呢?另外,如果目标平台强制规定每个类必须是同一个根类(< code>Object)的(间接)子类型,那么在高级语言中就不能解决这个问题。

特征和组合

许多人认为Java和. net中的“一个类和多个接口”权衡过于严格。例如,它使得在不同类之间共享一些接口方法的通用默认实现变得困难。实际上,随着时间的推移Java和. Net设计者似乎得出了相同的结论,并推出了他们自己的解决此类问题的方法:在. Net中的扩展方法,然后在Java中的默认方法。Scala设计者添加了一个名为Mixins的功能,该功能在许多实际情况下表现良好。然而,与许多其他具有类似功能的动态语言不同,Scala仍然必须满足“恰好一个基类”规则和目标平台的其他限制。

重要的是要注意,在实践中使用mixins时,有一些重要的场景是实现装饰器或适配器模式的变体,这两者都依赖于这样一个事实,即您可以将基类型限制为比AnyAnyRef更具体的东西。这种用法的最好例子是scala.collection package。

Scala语法

所以现在你有以下目标/限制:

  1. 每个类只有一个基类。
  2. 能够从mixin向类添加逻辑。
  3. 支持具有受限基类型的mixin
  4. 从Scala看,来自目标平台(Java)的类被映射到Scala类(因为它们还可以映射到什么?)它们是预先编译的,我们不想干扰它们的实现。
  5. 其他优秀品质,如简单性、类型安全性、决定论等

如果你想要在你的语言中使用某种多重继承支持,你需要开发冲突解决规则:当几个基类型提供一些适合类中相同“槽”的逻辑时会发生什么。在禁止性状中的字段之后,我们留下了以下“插槽”:

    < li >目标平台方面的基类 < li >构造函数 < li >具有相同名称和签名的方法

可能的冲突解决策略是:

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

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

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

我们希望不同槽的规则是相同的,因为否则不清楚如何实现安全性(如果特征 AB 都覆盖了方法 foobar,foobar 的解析规则不同,则 AB 的不变量可能很容易被破坏)。Scala的方法基于类线性化。简而言之,这些是以某种预测方式将基类的层次结构“扁平化”为简单线性结构的方法,该方式基于链中的左类型的想法 - 它越“基”(继承越高)。完成此操作后,方法的冲突解决规则变得简单:您通过超级调用浏览基类型和链行为的列表;如果未调用 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

按<code>t的顺序排列。获取值和t秒。是否应调用getValue?显然,CThird已经编译,您无法更改它的super,因此它必须移动到第一个位置,并且已经有了TSecond。getValue在其内部调用。但另一方面,这打破了左边的一切都是基础,右边的一切是孩子的规则。不引入这种混淆的最简单方法是强制执行非特征类必须优先的规则。

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

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

同样,另一个孩子可以扩展的唯一非特征类是CThird,这也使得冲突解决规则很难推理。

这就是为什么 Scala 使规则变得更加简单:任何提供基类的东西都必须来自第一个位置。然后,将相同的规则扩展到特征上也是有意义的,因此,如果第一个位置被某些特征占据 - 它也定义了基类。

 类似资料:
  • 在Scala表示法中,是一个函数,它接受任何类型并将其映射到所有列表类型集合中的一个类型,例如它将类型映射到类型并映射上的函数 到 到 现在,的每个实例都是一个monoid,具有函数(在Haskell中为)和函数(在Haskell中为)。我的猜测是,可以使用列表是单ID这一事实来说明必须映射列表的所有元素。我在这里的感觉是,如果从Applicative中添加函数,就会得到一个列表,其中只有一个其他

  • 本文向大家介绍在.vue文件中style是必须的吗?那script是必须的吗?为什么?相关面试题,主要包含被问及在.vue文件中style是必须的吗?那script是必须的吗?为什么?时的应答技巧和注意事项,需要的朋友参考一下 style 不是必须的,script 是必须的,而且必须要写上

  • Java要求,如果您在构造函数中调用this()或super(),它必须是第一条语句。为什么? 例如: Sun编译器说“调用super必须是构造函数中的第一条语句”。Eclipse编译器说“构造函数调用必须是构造函数中的第一条语句”。 但是,您可以通过稍微重新排列代码来解决这个问题: 下面是另一个例子: 因此,它不会阻止您在调用super之前执行逻辑。它只是阻止您执行无法容纳在单个表达式中的逻辑。

  • 问题内容: Java要求,如果你在构造函数中调用this()或super(),则它必须是第一条语句。为什么? 例如: Sun编译器说“对super的调用必须是构造函数中的第一条语句”。Eclipse编译器说“构造函数调用必须是构造函数中的第一条语句”。 但是,你可以通过重新安排一些代码来解决此问题: 这是另一个示例: 因此,这不会阻止你在调用super之前执行逻辑。这只是阻止你执行无法包含在单个表

  • 问题内容: Java Bean是否必须实现接口? 问题答案: 这是Javabeans规范中描述的“典型”功能之一。 这是第 2.1 章的摘录 什么是bean? 各个Java Bean支持的功能会有所不同,但是区分Java Bean的典型统一功能是: 支持“自省”,以便构建器工具可以分析bean的工作方式 支持“自定义”,以便在使用应用程序构建器时,用户可以自定义Bean的外观和行为。 支持“事件”

  • 问题内容: 为什么字典键必须是不可变的?我正在寻找一个简单明了的原因,为什么Python字典中的键具有该限制。 问题答案: 在我的计算机上,有一个包含大量英语单词的文件: 让我们创建一个字典来存储所有这些单词的长度: 并且,为了踢球,我们将改组原始单词列表: 嗯,滚刀。无论如何…现在我们已经有点混乱了,我们变得有点偏执了(可能出于与渴望滚刀相同的原因),并且我们想检查字典中的所有单词是否都正确。我