在Scala spec,据说在类模板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 {}
我的问题是:
class Sub extends Mt1, Mt2
)或特征模板(例如trait Sub extends Mt1, Mt2
)?免责声明:我不是,也不是“斯卡拉设计委员会”的成员或类似的东西,所以关于“为什么?”的答案问题主要是猜测,但我认为是有用的。
免责声明#2:我已经写了几个小时的这篇文章,并且几次拍摄,所以它可能不是很一致
免责声明#3(对未来的读者来说是一种可耻的自我推销):如果你觉得这个很长的答案很有用,你也可以看看 my another long answer 对Lifu Huang在类似主题上提出的另一个问题。
简短的答案
这是一个复杂的事情,除非你已经知道答案是什么,否则我不认为有一个很好的简短回答。虽然我的答案很长,但这是我最好的答案:
为什么父列表中的第一个基类必须是非特征类?
因为必须只有一个非特质基类,如果它始终是第一个,它会使事情变得更容易
- 我的理解是否正确?
是的,您的隐含示例是将要发生的事情。但是我不确定它是否完全理解了这个主题。
- 此要求(父列表中的第一个子类必须是非特征类)和隐式扩展是否仅适用于类模板(例如
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子句
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
作为基类。
- 为什么这个要求和隐式扩展是必要的?
主要有三个原因:
Scala spec目标/意图
我相信,当读一个规范时,有两组不同的问题:
实际上我认为在很多情况下#2比#1更重要,但不幸的是,规范很少明确包含对该领域的见解。无论如何,我将从我对#2的猜测开始:Scala中类系统的意图/目标/限制是什么?主要的高级目标是创建一个比Java或.Net(非常相似)更丰富的类型系统,但可以是:
旁注:.Net的支持在几年前就被取消了,但多年来它一直是目标平台之一,这影响了设计。
单基类
简短摘要:本节描述了为什么Scala设计师有强烈动机在语言中使用“完全一个基类”规则的一些原因。
OO设计和特别是继承的一个主要问题是AFAIK的问题是:“好的和有用的”做法和“坏的”做法之间的界限究竟在哪里?“开了。这意味着每种语言都必须找出自己的权衡,使不可能出错的事情和使有用的事情成为可能(并且容易)。许多人认为,在C ++中,这显然是Java和.Net的主要灵感,这种权衡过度转移到“允许所有内容,即使它是潜在有害的”区域。它使许多新语言的设计者寻求更多限制性的权衡。特别是JVM和.Net平台都强制执行所有类型被分为“值类型”(又名原始类型),“类”和“接口”的规则,除了根类(java.lang.Object
/ System.Object
)之外,每个类都只有一个“基类”和零个或多个“基本接口”。这个决定是对multiple inheritance的许多问题的反应,包括臭名昭着的“钻石问题”,但实际上还有许多其他问题。
Sidenote(关于内存布局):多重继承的另一个主要问题是内存中的对象布局。考虑下面由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
}
}
这里棘手的部分是如何在对象的内存中实际放置achillesPos
和tortoisePos
字段。问题是您可能希望只有一个内存中所有方法的编译副本,并且您希望代码高效。这意味着getAchillesPos
和stepAchilles
应该知道achillesPos
关于this
指针的一些固定偏移量。类似地,getTortoisePos
和stepTortoise
应该知道tortoisePos
关于this
指针的一些固定偏移量。你实现这一目标所需的所有选择看起来并不好看。例如:
achillesPos
总是第一名,tortoisePos
总是第二名。但这意味着在TortoiseImpl
的情况下,tortoisePos
也应该是第二个字段,但没有任何东西可以填充第一个字段,所以你浪费了一些记忆。此外,如果AchillesImpl
和TortoiseImpl
都来自预编译库,您应该有一些方法来移动对它们中的字段的访问。this
时,你可能会尝试“动态”修复TortoiseImpl
指针(AFAIK这是C ++真正起作用的方式)。当TortoiseImpl
是一个抽象类,通过trait Achilles
知道class AchillesImpl
(但不是特定的extends
)并尝试从那里通过this
回调一些方法或通过this
到一些以Achilles
为参数的方法时,这变得特别有趣,所以this
必须“修复”。请注意,这与“钻石问题”不同,因为所有字段和实现只有一个副本。在实际的Scala中,这个问题不存在,因为trait
无法真正声明任何字段。当您在特征中声明val
或var
时,您实际上声明了一个getter(和一个setter)方法,这些方法将由扩展特征的特定类实现,并且每个类都可以完全控制字段的布局。实际上,就性能而言,这很可能正常,因为JVM(JIT)可以在许多真实场景中内联这样的虚拟调用。
旁注结束
另一个要点是与目标平台的互操作性。即使Scala以某种方式支持真正的多重继承,所以你可以拥有一个继承自String with Date
的类型,并且可以传递给期望String
和期望Date
的两种方法,从Java的角度来看这会是什么样子?此外,如果目标平台强制执行规则,即每个类必须是同一根类(Object
)的(间接)子类型,则无法在更高级别的语言中使用它。
特质和混合
许多人认为用Java和.Net制作的“一类和多接口”权衡过于严格。例如,它使得很难在不同类之间共享某些接口方法的公共默认实现。实际上,Java和.Net设计人员似乎得出了相同的结论并推出了针对此类问题的修复程序:.net中的Extension methods,然后是Java中的Default methods。 Scala设计师添加了一个名为Mixins的功能,在许多实际情况下,这个功能都很出色。然而,与许多具有类似功能的其他动态语言不同,Scala仍然必须满足“完全一个基类”规则和目标平台的其他限制。
重要的是要注意,在实践中使用mixins的一些重要场景是实现Decorator或Adapter模式的变体,这两种模式都依赖于这样一个事实,即你可以将你的基类型限制为比Any
或AnyRef
更具体的东西。这种用法的主要例子是scala.collection
包。
Scala语法
所以现在你有以下目标/限制:
如果您希望在您的语言中使用某种多重继承支持,则需要开发冲突解决规则:当几种基类型提供适合您类中相同“槽”的逻辑时会发生什么。在禁止特征字段后,我们留下以下“插槽”:
可能的冲突解决策略是:
从某种意义上说,Scala使用所有可用的(即前3个)策略,但高级目标是:让我们尽可能多地保留逻辑。
本讨论最重要的部分是构造函数和方法的冲突解决。
我们希望不同时段的规则相同,否则不清楚如何实现安全性(如果特征A
和B
都覆盖方法foo
和bar
但foo
和bar
的分辨率规则不同,A
和B
的不变量可能很容易被打破)。 Scala的方法基于class linearization。简而言之,这是以某种预测方式将基类的层次结构“扁平化”为简单线性结构的方式,这种方式基于with
链中的lefter类型 - 更多“基数”(继承中更高)的概念它是。执行此操作后,方法的冲突解决规则变得简单:您通过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.getValue
和TSecond.getValue
的顺序?显然CThird
已经编译,你不能改变它的super
,所以它必须被移动到第一个位置,并且已经有TSecond.getValue
调用它。但另一方面,这打破了左边的一切都是基础的规则,右边的一切都是孩子。不引入这种混淆的最简单方法是强制执行非特质类必须先行的规则。
如果您只是通过用扩展它的class CThird
替换trait
来扩展前面的示例,则适用相同的逻辑:
trait TFourth extends CThird
class AnotherChild extends TFirst with TSecond with TFourth
同样,唯一的非特质类AnotherChild
可以扩展是CThird
,这再次使冲突解决规则很难推理。
这就是为什么Scala使规则更简单:无论提供哪种基类必须来自第一个位置。然后将相同的规则扩展到特征上是有意义的,因此如果第一个位置被某个特征占用 - 它还定义了基类。
1)基本上是的,你的理解是正确的。就像在Java中一样,每个类都继承自java.lang.Object
(Scala中的AnyRef
)。因此,既然您正在定义一个具体的类,那么您将隐式继承自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”段落中:
与普通类不同,traits不能具有构造函数参数。此外,没有构造函数参数传递给特征的超类。这是没有必要的,因为在初始化超类之后初始化了traits。
假设特征D定义了类型C的实例x的某些方面(即D是C的基类)。然后,x中D的实际超类型是由L(C)中所有成功D的基类组成的复合类型。
这是定义没有参数的基础构造函数所必需的。
3)根据答案(2),需要定义基础构造函数