类和对象

TmplDef          ::= [‘case’] ‘class’ ClassDef
                  |  [‘case’] ‘object’ ObjectDef
                  |  ‘trait’ TraitDef

对象 都用模板来定义。

模板

ClassTemplate   ::=  [EarlyDefs] ClassParents [TemplateBody]
TraitTemplate   ::=  [EarlyDefs] TraitParents [TemplateBody]
ClassParents    ::=  Constr {‘with’ AnnotType}
TraitParents    ::=  AnnotType {‘with’ AnnotType}
TemplateBody    ::=  [nl] ‘{’ [SelfType] TemplateStat {semi TemplateStat} ‘}’
SelfType        ::=  id [‘:’ Type] ‘=>’
                 |   this ‘:’ Type ‘=>’

模板定义了对象或单个对象的特征、行为和初始状态。模板是实例创建表达式、类定义和对象定义的一部分。模板 ´sc´ with ´mt_1´ with ... with ´mt_n´ { ´\mathit{stats}´ } 由一个构造函数调用 ´sc´ 组成,它定义了模板的超类,特征引用 ´mt_1, ..., mt_n´ ´(n \geq 0)´,它定义了模板的特征,以及一个语句序列 ´\mathit{stats}´,它包含初始化代码和模板的附加成员定义。

每个特征引用 ´mt_i´ 必须表示一个 特征。相反,超类构造函数 ´sc´ 通常引用一个不是特征的类。可以编写一个以特征引用开头的父类列表,例如 ´mt_1´ with ... with ´mt_n´。在这种情况下,父类列表会隐式扩展为包含 ´mt_1´ 的超类型作为第一个父类型。新的超类型必须至少有一个不带参数的构造函数。在下文中,我们将始终假设已经执行了这种隐式扩展,因此模板的第一个父类是一个常规的超类构造函数,而不是一个特征引用。

模板的父类列表必须格式正确。这意味着由超类构造函数 ´sc´ 表示的类必须是所有特征 ´mt_1, ..., mt_n´ 的超类的子类。换句话说,模板继承的非特征类在继承层次结构中形成一条链,从模板的超类开始。

模板的超类构造函数 ´sc´ 不得是 枚举类,除非模板是 ´sc´ 的 枚举情况 的实现。

模板的最小适当超类型是包含其所有父类类型的类类型或 复合类型

语句序列 ´\mathit{stats}´ 包含成员定义,这些定义定义新成员或覆盖父类中的成员。如果模板是抽象类或特征定义的一部分,则语句部分 ´\mathit{stats}´ 也可能包含抽象成员的定义。如果模板是具体类定义的一部分,则 ´\mathit{stats}´ 仍然可以包含抽象类型成员的定义,但不能包含抽象项成员的定义。此外,´\mathit{stats}´ 在任何情况下也可能包含表达式;这些表达式按给定的顺序执行,作为模板初始化的一部分。

模板语句序列可以在前面加上形式参数定义和箭头,例如 ´x´ =>´x´:´T´ =>。如果给出了形式参数,则可以在整个模板主体中将其用作对引用 this 的别名。如果形式参数带有类型 ´T´,则此定义会影响底层类或对象的自类型 ´S´,如下所示:令 ´C´ 为定义模板的类或特征或对象的类型。如果为形式自参数给出了类型 ´T´,则 ´S´ 是 ´T´ 和 ´C´ 的最大下界。如果没有给出类型 ´T´,则 ´S´ 只是 ´C´。在模板内部,假设 this 的类型为 ´S´。

类或对象的自类型必须符合模板 ´t´ 继承的所有类的自类型。

自类型注释的第二种形式只是 this: ´S´ =>。它为 this 指定类型 ´S´,而不会为其引入别名。

示例

考虑以下类定义

class Base extends Object {}
trait Mixin extends Base {}
object O extends Mixin {}

在这种情况下,O 的定义扩展为

object O extends Base with Mixin {}

从 Java 类型继承

模板可以具有 Java 类作为其超类,以及 Java 接口作为其混合。

模板评估

考虑一个模板 ´sc´ with ´mt_1´ with ´mt_n´ { ´\mathit{stats}´ }

如果这是一个 特质 的模板,那么它的混合评估包含对语句序列 ´\mathit{stats}´ 的评估。

如果这不是一个特质的模板,那么它的评估包含以下步骤。

构造函数调用

Constr  ::=  AnnotType {‘(’ [Exprs] ‘)’}

构造函数调用定义了由实例创建表达式创建的对象的类型、成员和初始状态,或者定义了类或对象定义继承的某个对象定义部分。构造函数调用是一个方法应用 ´x´.´c´[´\mathit{targs}´](´\mathit{args}_1´)...(´\mathit{args}_n´),其中 ´x´ 是一个 稳定标识符,´c´ 是一个类型名称,它要么指定一个类,要么为一个类定义一个别名类型,´\mathit{targs}´ 是一个类型参数列表,´\mathit{args}_1, ..., \mathit{args}_n´ 是参数列表,并且该类有一个构造函数,它 适用于 给定的参数。如果构造函数调用使用命名参数或默认参数,它将使用与 此处 描述的相同转换转换为块表达式。

可以省略前缀 ´x´.。只有当类 ´c´ 接受类型参数时,才能给出类型参数列表。即使这样也可以省略,在这种情况下,将使用 局部类型推断 合成类型参数列表。如果没有给出显式参数,则隐式提供一个空列表 ()

对构造函数调用 ´x´.´c´[´\mathit{targs}´](´\mathit{args}_1´)...(´\mathit{args}_n´) 的评估包含以下步骤

类线性化

通过从类 ´C´ 的直接继承关系的传递闭包可达的类称为 ´C´ 的基类。由于混合类的存在,基类上的继承关系通常形成有向无环图。此图的线性化定义如下。

定义:线性化

令 ´C´ 为一个类,其模板为 ´C_1´ with ... with ´C_n´ { ´\mathit{stats}´ }`。´C´ 的线性化,´\mathcal{L}(C)´ 定义如下:$$ \mathcal{L}(C) = C, \mathcal{L}(C_n) \; \vec{+} \; ... \; \vec{+} \; \mathcal{L}(C_1) $$

这里 ´\vec{+}´ 表示连接,其中右操作数的元素替换左操作数中的相同元素

$$ \begin{array}{lcll} {a, A} \;\vec{+}\; B &=& a, (A \;\vec{+}\; B) &{\bf if} \; a \not\in B \\ &=& A \;\vec{+}\; B &{\bf if} \; a \in B \end{array} $$

示例

考虑以下类定义。

abstract class AbsIterator extends AnyRef { ... }
trait RichIterator extends AbsIterator { ... }
class StringIterator extends AbsIterator { ... }
class Iter extends StringIterator with RichIterator { ... }

然后类 Iter 的线性化为

{ Iter, RichIterator, StringIterator, AbsIterator, AnyRef, Any }

注意,类的线性化细化了继承关系:如果 ´C´ 是 ´D´ 的子类,则 ´C´ 在任何包含 ´C´ 和 ´D´ 的线性化中都位于 ´D´ 之前。 线性化 还满足这样的性质,即类的线性化始终包含其直接超类的线性化作为后缀。

例如,StringIterator 的线性化为

{ StringIterator, AbsIterator, AnyRef, Any }

它是其子类 Iter 的线性化的后缀。对于混合类的线性化,情况并非如此。例如,RichIterator 的线性化为

{ RichIterator, AbsIterator, AnyRef, Any }

它不是 Iter 的线性化的后缀。

类成员

由模板 ´C_1´ with ... with ´C_n´ { ´\mathit{stats}´ } 定义的类 ´C´ 可以在其语句序列 ´\mathit{stats}´ 中定义成员,并可以从所有父类继承成员。Scala 采用 Java 和 C# 的静态方法重载约定。因此,一个类可能定义和/或继承多个具有相同名称的方法。为了确定类 ´C´ 的定义成员是否覆盖了父类的成员,或者两者是否在 ´C´ 中作为重载变体共存,Scala 使用以下关于成员匹配的定义

定义:匹配

如果 ´M´ 和 ´M'´ 绑定相同的名称,并且以下之一成立,则成员定义 ´M´ 匹配成员定义 ´M'´。

  1. ´M´ 和 ´M'´ 都不是方法定义。
  2. ´M´ 和 ´M'´ 都定义了具有等效参数类型的单态方法。
  3. ´M´ 在 Java 中定义,并定义了一个具有空参数列表 () 的方法,而 ´M'´ 定义了一个无参数方法。
  4. ´M´ 和 ´M'´ 定义了两个具有相同参数类型数量 ´\overline T´、´\overline T'´ 和相同类型参数数量 ´\overline t´、´\overline t'´ 的多态方法,假设 ´\overline T' = [\overline t'/\overline t]\overline T´。

成员定义分为两类:具体和抽象。类 ´C´ 的成员要么是直接定义的(即它们出现在 ´C´ 的语句序列 ´\mathit{stats}´ 中),要么是继承的。有两个规则决定了类的成员集,每个类别一个规则。

类 ´C´ 的具体成员是某个类 ´C_i \in \mathcal{L}(C)´ 中的任何具体定义 ´M´,除非存在一个前面的类 ´C_j \in \mathcal{L}(C)´,其中 ´j < i´ 直接定义了与 ´M´ 匹配的具体成员 ´M'´。

类 ´C´ 的抽象成员是某个类 ´C_i \in \mathcal{L}(C)´ 中的任何抽象定义 ´M´,除非 ´C´ 已经包含与 ´M´ 匹配的具体成员 ´M'´,或者存在一个前面的类 ´C_j \in \mathcal{L}(C)´,其中 ´j < i´ 直接定义了与 ´M´ 匹配的抽象成员 ´M'´。

此定义还确定了类 ´C´ 及其父类中匹配成员之间的覆盖关系。首先,具体定义总是覆盖抽象定义。其次,对于既是具体定义又是抽象定义的定义 ´M´ 和 ´M´',如果 ´M´ 出现在比定义 ´M'´ 的类更早的类(在 ´C´ 的线性化中),则 ´M´ 覆盖 ´M'´。

如果模板直接定义了两个匹配的成员,则会发生错误。如果模板包含两个具有相同名称和相同擦除类型的成员(直接定义或继承),也会发生错误。最后,模板不允许包含两个具有相同名称的方法(直接定义或继承),这两个方法都定义了默认参数。

示例

考虑以下特征定义

trait A { def f: Int }
trait B extends A { def f: Int = 1 ; def g: Int = 2 ; def h: Int = 3 }
trait C extends A { override def f: Int = 4 ; def g: Int }
trait D extends B with C { def h: Int }

然后特征 D 具有直接定义的抽象成员 h。它从特征 C 继承成员 f,从特征 B 继承成员 g

覆盖

类 ´C´ 的成员 ´M´ 与 ´C´ 基类的非私有成员 ´M'´匹配,则称该成员覆盖该成员。在这种情况下,覆盖成员 ´M´ 的绑定必须包含被覆盖成员 ´M'´ 的绑定。此外,以下关于修饰符的限制适用于 ´M´ 和 ´M'´

class X { val stable = 1}
class Y extends X { override var stable = 1 } // error

另一个限制适用于抽象类型成员:具有 易变类型 作为其上限的抽象类型成员不能重写没有易变上限的抽象类型成员。

一个特殊规则涉及无参数方法。如果一个定义为 def ´f´: ´T´ = ...def ´f´ = ... 的无参数方法重写了在 Java 中定义的类型为 ´()T'´ 的方法,该方法具有空参数列表,那么 ´f´ 也被认为具有空参数列表。

重写方法从超类中的定义继承所有默认参数。通过在重写方法中指定默认参数,可以添加新的默认值(如果超类中的对应参数没有默认值)或覆盖超类的默认值(否则)。

示例

考虑以下定义:

trait Root { type T <: Root }
trait A extends Root { type T <: A }
trait B extends Root { type T <: B }
trait C extends A with B

那么类定义 C 就不完善,因为 TC 中的绑定是 type T <: B,它不能包含 T 在类型 A 中的绑定 type T <: A。可以通过在类 C 中添加 T 类型的重写定义来解决这个问题。

class C extends A with B { type T <: C }

继承闭包

设 ´C´ 为一个类类型。´C´ 的继承闭包是最小的类型集 ´\mathscr{S}´,满足以下条件:

如果类类型的继承闭包包含无限多个类型,则为静态错误。(此限制对于使子类型化可判定是必要的1)。

修饰符

Modifier          ::=  LocalModifier
                    |  AccessModifier
                    |  ‘override’
LocalModifier     ::=  ‘abstract’
                    |  ‘final’
                    |  ‘sealed’
                    |  ‘implicit’
                    |  ‘lazy’
                    |  ‘infix’
AccessModifier    ::=  (‘private’ | ‘protected’) [AccessQualifier]
AccessQualifier   ::=  ‘[’ (id | ‘this’) ‘]’

成员定义之前可以添加修饰符,这些修饰符会影响由它们绑定的标识符的可访问性和使用方式。如果给出多个修饰符,它们的顺序无关紧要,但同一个修饰符不能出现多次。在重复定义之前出现的修饰符适用于所有组成定义。控制修饰符有效性和含义的规则如下。

private

private 修饰符可用于模板中的任何定义。模板的私有成员只能从直接封闭模板及其伴随模块或 伴随类 中访问。

private 修饰符对于 顶层 模板也是有效的。

private 修饰符可以用标识符 ´C´(例如 private[´C´])进行限定,该标识符必须表示封闭定义的类或包。用此类修饰符标记的成员分别只能从包 ´C´ 内部的代码或只能从类 ´C´ 内部的代码及其 伴随模块 中访问。

限定的另一种形式是 private[this]。用此修饰符标记的成员 ´M´ 称为对象保护;它只能从定义它的对象内部访问。也就是说,选择 ´p.M´ 只有在前缀为 this´O´.this 时才合法,其中 ´O´ 是封闭引用的某个类。此外,还适用未限定 private 的限制。

标记为私有且没有限定符的成员称为类私有,而标记为 private[this] 的成员称为对象私有。如果成员是类私有或对象私有,则该成员是私有的,但如果它标记为 private[´C´] 其中 ´C´ 是一个标识符,则不是;在后一种情况下,该成员称为限定私有

类私有或对象私有成员不能是抽象的,也不能具有 protectedoverride 修饰符。它们不会被子类继承,也不能覆盖父类中的定义。

protected

protected 修饰符适用于类成员定义。类的受保护成员可以从以下位置访问

protected 修饰符可以用标识符 ´C´(例如 protected[´C´])进行限定,该标识符必须表示封闭定义的类或包。用此类修饰符标记的成员也可以分别从包 ´C´ 内部的所有代码或从类 ´C´ 内部的所有代码及其 伴随模块 中访问。

受保护的标识符 ´x´ 只能在以下情况之一适用时用作选择 ´r´.´x´ 中的成员名称

另一种限定形式是 protected[this]。用此修饰符标记的成员 ´M´ 称为对象保护;它只能从定义它的对象内部访问。也就是说,选择 ´p.M´ 只有在前缀是 this´O´.this 时才合法,其中 ´O´ 是包含该引用的某个类。此外,还适用非限定 protected 的限制。

覆盖

override 修饰符适用于类成员定义。对于在父类中覆盖其他具体成员定义的成员定义,它是强制性的。如果给出了 override 修饰符,则必须至少有一个被覆盖的成员定义(具体或抽象)。

抽象覆盖

override 修饰符与 abstract 修饰符结合使用时具有额外的意义。该修饰符组合仅允许用于特性的值成员。

如果模板的成员 ´M´ 是抽象的,或者它被标记为 abstractoverride 并且 ´M´ 覆盖的每个成员再次不完整,则我们称该成员 ´M´ 为不完整

请注意,abstract override 修饰符组合不会影响成员是具体还是抽象的概念。

抽象

abstract 修饰符用于类定义。对于特性,它是多余的,对于所有具有不完整成员的其他类,它是强制性的。抽象类不能用构造函数调用实例化,除非后面跟着混合和/或细化,这些混合和/或细化覆盖了类的所有不完整成员。只有抽象类和特性可以具有抽象术语成员。

abstract 修饰符也可以与 override 结合使用,用于类成员定义。在这种情况下,前面讨论的内容适用。

最终

final 修饰符适用于类成员定义和类定义。final 类成员定义不能在子类中被覆盖。final 类不能被模板继承。final 对对象定义是多余的。最终类或对象的成员隐式地也是最终的,因此 final 修饰符通常对它们也是多余的。但是请注意,常量值定义 确实需要显式的 final 修饰符,即使它们是在最终类或对象中定义的。final 允许用于抽象类,但不能应用于特性或不完整成员,也不能在一个修饰符列表中与 sealed 结合使用。

密封

sealed 修饰符应用于类定义。sealed 类不能直接继承,除非继承模板定义在与继承类相同的源文件中。但是,密封类的子类可以在任何地方继承。

延迟

lazy 修饰符应用于值定义。lazy 值在第一次访问时初始化(可能根本不会发生)。在初始化期间尝试访问延迟值可能会导致循环行为。如果在初始化期间抛出异常,则该值被认为未初始化,并且以后的访问将尝试重新评估其右侧。

中缀

infix 修饰符应用于方法定义和类型定义。它表示该方法或类型旨在用于中缀位置,即使它具有字母数字名称。

如果一个方法覆盖了另一个方法,则它们的 infix 注释必须一致。要么都用 infix 注释,要么都不注释。

infix 方法的第一个非接收器参数列表必须定义正好一个参数。示例

infix def op1(x: S): R             // ok
infix def op2[T](x: T)(y: S): R    // ok
infix def op3[T](x: T, y: S): R    // error: two parameters
extension (x: A)
  infix def op4(y: B): R          // ok
  infix def op5(y1: B, y2: B): R  // error: two parameters

infix 修饰符也可以用于具有正好两个类型参数的类型、特征或类定义。像

infix type op[X, Y]

这样的中缀类型可以使用中缀语法应用,即 A op B

示例

以下代码说明了限定私有的使用

package outerpkg.innerpkg
class Outer {
  class Inner {
    private[Outer] def f()
    private[innerpkg] def g()
    private[outerpkg] def h()
  }
}

这里,对方法 f 的访问可以在 Outer 中的任何地方出现,但在其外部则不能。对方法 g 的访问可以在包 outerpkg.innerpkg 中的任何地方出现,就像 Java 中的包私有方法一样。最后,对方法 h 的访问可以在包 outerpkg 中的任何地方出现,包括它包含的包。

示例

一个有用的习惯用法来阻止类的客户端构造该类的新的实例是声明该类为 abstractsealed

object m {
  abstract sealed class C (x: Int) {
    def nextC = new C(x + 1) {}
  }
  val empty = new C(0) {}
}

例如,在上面的代码中,客户端只能通过调用现有 m.C 对象的 nextC 方法来创建类 m.C 的实例;客户端无法直接创建类 m.C 的对象。事实上,以下两行都有错误

new m.C(0)    // **** error: C is abstract, so it cannot be instantiated.
new m.C(0) {} // **** error: illegal inheritance from sealed class.

可以通过将主构造函数标记为 private 来实现类似的访问限制(示例)。

类定义

TmplDef           ::=  ‘class’ ClassDef
ClassDef          ::=  id [TypeParamClause] {Annotation}
                       [AccessModifier] ClassParamClauses ClassTemplateOpt
ClassParamClauses ::=  {ClassParamClause}
                       [[nl] ‘(’ implicit ClassParams ‘)’]
ClassParamClause  ::=  [nl] ‘(’ [ClassParams] ‘)’
ClassParams       ::=  ClassParam {‘,’ ClassParam}
ClassParam        ::=  {Annotation} {Modifier} [(‘val’ | ‘var’)]
                       id [‘:’ ParamType] [‘=’ Expr]
ClassTemplateOpt  ::=  ‘extends’ ClassTemplate | [[‘extends’] TemplateBody]

类定义最通用的形式是

class ´c´[´\mathit{tps}\,´] ´as´ ´m´(´\mathit{ps}_1´)...(´\mathit{ps}_n´) extends ´t´    ´\quad(n \geq 0)´.

这里,

此类定义定义了一个类型 ´c´[´\mathit{tps}\,´] 和一个构造函数,当应用于符合类型 ´\mathit{ps}´ 的参数时,通过评估模板 ´t´ 初始化类型 ´c´[´\mathit{tps}\,´] 的实例。

示例 - valvar 参数

以下示例说明了类 Cvalvar 参数

class C(x: Int, val y: String, var z: List[String])
val c = new C(1, "abc", List())
c.z = c.y :: c.z
示例 - 私有构造函数

以下类只能从其伴生模块创建。

object Sensitive {
  def makeSensitive(credentials: Certificate): Sensitive =
    if (credentials == Admin) new Sensitive()
    else throw new SecurityViolationException
}
class Sensitive private () {
  ...
}

构造函数定义

FunDef         ::= ‘this’ ParamClause ParamClauses
                   (‘=’ ConstrExpr | [nl] ConstrBlock)
ConstrExpr     ::= SelfInvocation
                |  ConstrBlock
ConstrBlock    ::= ‘{’ SelfInvocation {semi BlockStat} ‘}’
SelfInvocation ::= ‘this’ ArgumentExprs {ArgumentExprs}

除了主构造函数之外,类可能还有其他构造函数。这些由形式为 def this(´\mathit{ps}_1´)...(´\mathit{ps}_n´) = ´e´ 的构造函数定义定义。这样的定义为封闭类引入了另一个构造函数,其参数如形式参数列表 ´\mathit{ps}_1 , ..., \mathit{ps}_n´ 中给出,其评估由构造函数表达式 ´e´ 定义。每个形式参数的作用域是后续的参数部分和构造函数表达式 ´e´。构造函数表达式要么是自构造函数调用 this(´\mathit{args}_1´)...(´\mathit{args}_n´),要么是块,该块以自构造函数调用开头。自构造函数调用必须构造类的泛型实例。即,如果所讨论的类名为 ´C´ 且类型参数为 [´\mathit{tps}\,´],则自构造函数调用必须生成 ´C´[´\mathit{tps}\,´] 的实例;不允许实例化形式类型参数。

构造函数定义的签名和自构造函数调用在封闭类定义点有效的范围内进行类型检查和评估,并由封闭类的任何类型参数增强。构造函数表达式的其余部分在当前类中作为方法体进行类型检查和评估。

如果类 ´C´ 有辅助构造函数,它们与 ´C´ 的主 构造函数 共同构成一个重载的构造函数定义。对于 ´C´ 的构造函数调用,包括构造函数表达式本身中的自构造函数调用,都适用 重载解析 的通常规则。但是,与其他方法不同,构造函数永远不会被继承。为了防止构造函数调用无限循环,有一个限制,即每个自构造函数调用必须引用一个在其之前的构造函数定义(即它必须引用前面的辅助构造函数或类的主构造函数)。

示例

考虑以下类定义

class LinkedList[A]() {
  var head: A = _
  var tail: LinkedList[A] = null
  def this(head: A) = { this(); this.head = head }
  def this(head: A, tail: LinkedList[A]) = { this(head); this.tail = tail }
}

这定义了一个名为 LinkedList 的类,它有三个构造函数。第二个构造函数构造一个单例列表,而第三个构造函数构造一个具有给定头和尾的列表。

案例类

TmplDef  ::=  ‘case’ ‘class’ ClassDef

如果类定义以 case 为前缀,则该类被称为案例类

案例类必须有一个非隐式的参数部分。第一个参数部分中的形式参数被称为元素,它们被特殊对待。首先,可以通过构造函数模式将此类参数的值提取为字段。其次,除非参数已经带有 valvar 修饰符,否则会隐式地向此类参数添加 val 前缀。因此,将为参数 生成 一个访问器定义。

´c´[´\mathit{tps}\,´](´\mathit{ps}_1\,´)...(´\mathit{ps}_n´) 的案例类定义,其中 ´\mathit{tps}´ 是类型参数,´\mathit{ps}´ 是值参数,意味着定义了一个伴生对象,它充当 提取器对象。它具有以下形状

object ´c´ {
  def apply[´\mathit{tps}\,´](´\mathit{ps}_1\,´)...(´\mathit{ps}_n´): ´c´[´\mathit{tps}\,´] = new ´c´[´\mathit{Ts}\,´](´\mathit{xs}_1\,´)...(´\mathit{xs}_n´)
  def unapply[´\mathit{tps}\,´](´x´: ´c´[´\mathit{tps}\,´]) =
    if (x eq null) scala.None
    else scala.Some(´x.\mathit{xs}_{11}, ... , x.\mathit{xs}_{1k}´)
}

这里,´\mathit{Ts}´ 代表类型参数部分 ´\mathit{tps}´ 中定义的类型向量,每个 ´\mathit{xs}_i´ 表示参数部分 ´\mathit{ps}_i´ 的参数名称,´\mathit{xs}_{11}, ... , \mathit{xs}_{1k}´ 表示第一个参数部分 ´\mathit{xs}_1´ 中所有参数的名称。如果类中缺少类型参数部分,则 applyunapply 方法中也会缺少类型参数部分。

如果伴生对象 ´c´ 已经定义,则 applyunapply 方法将被添加到现有对象中。如果对象 ´c´ 已经具有 匹配apply(或 unapply)成员,则不会添加新的定义。如果类 ´c´ 是 abstract,则会省略 apply 的定义。

如果 case class 定义包含一个空的 value 参数列表,则 unapply 方法返回一个 Boolean 而不是 Option 类型,定义如下

def unapply[´\mathit{tps}\,´](´x´: ´c´[´\mathit{tps}\,´]) = x ne null

如果 ´c´ 的第一个参数部分 ´\mathit{ps}_1´ 以一个 重复参数 结尾,则 unapply 方法的名称将更改为 unapplySeq

除非该类已经具有具有该名称的成员(直接定义或继承),或者该类具有重复参数,否则会隐式地向每个 case class 添加一个名为 copy 的方法。该方法定义如下

def copy[´\mathit{tps}\,´](´\mathit{ps}'_1\,´)...(´\mathit{ps}'_n´): ´c´[´\mathit{tps}\,´] = new ´c´[´\mathit{Ts}\,´](´\mathit{xs}_1\,´)...(´\mathit{xs}_n´)

同样,´\mathit{Ts}´ 代表类型参数部分 ´\mathit{tps}´ 中定义的类型向量,每个 ´xs_i´ 表示参数部分 ´ps'_i´ 的参数名称。第一个参数列表的 value 参数 ´ps'_{1,j}´ 具有形式 ´x_{1,j}´:´T_{1,j}´=this.´x_{1,j}´copy 方法的其他参数 ´ps'_{i,j}´ 定义为 ´x_{i,j}´:´T_{i,j}´。在所有情况下,´x_{i,j}´´T_{i,j}´ 都指代相应类参数 ´\mathit{ps}_{i,j}´ 的名称和类型。

除非 case class 本身已经提供了相同方法的定义,或者在 case class 的某个基类(不同于 AnyRef)中提供了相同方法的具体定义,否则每个 case class 都会隐式地覆盖类 scala.AnyRef 的某些方法定义。特别是

示例

以下是 lambda 演算的抽象语法定义

class Expr
case class Var   (x: String)          extends Expr
case class Apply (f: Expr, e: Expr)   extends Expr
case class Lambda(x: String, e: Expr) extends Expr

这定义了一个类 Expr,它具有 case class VarApplyLambda。lambda 表达式的按值调用评估器可以写成如下。

type Env = String => Value
case class Value(e: Expr, env: Env)

def eval(e: Expr, env: Env): Value = e match {
  case Var (x) =>
    env(x)
  case Apply(f, g) =>
    val Value(Lambda (x, e1), env1) = eval(f, env)
    val v = eval(g, env)
    eval (e1, (y => if (y == x) v else env1(y)))
  case Lambda(_, _) =>
    Value(e, env)
}

可以在程序的其他部分定义扩展类型 Expr 的其他 case class,例如

case class Number(x: Int) extends Expr

可以通过声明基类 Exprsealed 来排除这种可扩展性形式;在这种情况下,所有直接扩展 Expr 的类都必须与 Expr 位于同一个源文件中。

特征

TmplDef          ::=  ‘trait’ ClassDef

特征是一个旨在作为混入添加到其他类的类。此外,不会将构造函数参数传递给特征的超类。这是不必要的,因为特征在超类初始化后进行初始化。

假设特征 ´D´ 定义了类型 ´C´ 的实例 ´x´ 的某些方面(即 ´D´ 是 ´C´ 的基类)。然后 ´D´ 在 ´x´ 中的实际超类型是由 ´\mathcal{L}(C)´ 中所有在 ´D´ 之后出现的基类组成的复合类型。实际超类型为在特征中解析 super 引用 提供了上下文。请注意,实际超类型取决于在混入组合中添加特征的类型;它在定义特征时不是静态已知的。

如果 ´D´ 不是特征,那么它的实际超类型就是它的最小真超类型(它是静态已知的)。

示例

以下特征定义了与某些类型的对象进行比较的属性。它包含一个抽象方法 < 和其他比较运算符 <=>>= 的默认实现。

trait Comparable[T <: Comparable[T]] { self: T =>
  def < (that: T): Boolean
  def <=(that: T): Boolean = this < that || this == that
  def > (that: T): Boolean = that < this
  def >=(that: T): Boolean = that <= this
}
示例

考虑一个抽象类 Table,它实现从键类型 A 到值类型 B 的映射。该类具有一个 set 方法,用于将新的键/值对输入表,以及一个 get 方法,用于返回与给定键匹配的可选值。最后,还有一个 apply 方法,它类似于 get,但如果表对于给定键未定义,则返回给定的默认值。该类实现如下。

abstract class Table[A, B](defaultValue: B) {
  def get(key: A): Option[B]
  def set(key: A, value: B): Unit
  def apply(key: A) = get(key) match {
    case Some(value) => value
    case None => defaultValue
  }
}

这是 Table 类的具体实现。

class ListTable[A, B](defaultValue: B) extends Table[A, B](defaultValue) {
  private var elems: List[(A, B)] = Nil
  def get(key: A) = elems.find(_._1 == key).map(_._2)
  def set(key: A, value: B) = { elems = (key, value) :: elems }
}

这是一个特征,它阻止对父类的 getset 操作进行并发访问

trait SynchronizedTable[A, B] extends Table[A, B] {
  abstract override def get(key: A): B =
    synchronized { super.get(key) }
  abstract override def set(key: A, value: B) =
    synchronized { super.set(key, value) }
}

请注意,SynchronizedTable 并没有向其超类 Table 传递参数,即使 Table 定义了形式参数。还要注意,SynchronizedTablegetset 方法的 super 调用静态地引用了类 Table 中的抽象方法。只要调用方法标记为 abstract override,这都是合法的。

最后,以下混合组合创建了一个同步列表表,其中字符串作为键,整数作为值,默认值为 0

object MyTable extends ListTable[String, Int](0) with SynchronizedTable[String, Int]

对象 MyTableSynchronizedTable 继承了 getset 方法。这些方法中的 super 调用被重新绑定以引用 ListTable 中的相应实现,ListTableMyTableSynchronizedTable 的实际超类型。

扩展参数化特征

扩展带参数的特征需要额外的规则

  1. 如果类 ´C´ 扩展了参数化特征 ´T´,而其超类没有,则 ´C´ 必须´T´ 传递参数。

  2. 如果类 ´C´ 扩展了参数化特征 ´T´,而其超类也扩展了,则 ´C´ 不能´T´ 传递参数。

  3. 特征绝不能向父特征传递参数。

  4. 如果类 ´C´ 扩展了非参数化特征 ´T_i´,而 ´T_i´ 的基类型包含参数化特征 ´T_j´,并且 ´C´ 的超类没有扩展 ´T_j´,那么 ´C´ 必须 也显式扩展 ´T_j´ 并传递参数。如果缺少的特征只包含上下文参数,则此规则将放宽。在这种情况下,特征引用将作为具有推断参数的附加父级隐式插入。

示例 - 防止歧义

以下列表尝试使用不同的参数两次扩展 Greeting

trait Greeting(val name: String):
  def msg = s"How are you, $name"

class C extends Greeting("Bob")

class D extends C, Greeting("Bill") // error

@main def greet = println(D().msg)

此程序应该打印 "Bob" 还是 "Bill"?事实上,这个程序是非法的,因为它违反了上面的规则 2。相反,D 可以扩展 Greeting 而不传递参数。

示例 - 覆盖

这是一个覆盖 msgGreeting 变体

trait FormalGreeting extends Greeting:
  override def msg = s"How do you do, $name"

根据规则 4,扩展 FormalGreeting 的以下类需要也使用参数扩展 Greeting

class GreetBobFormally extends FormalGreeting, Greeting("Bob")
示例 - 推断上下文参数

这是一个 Greeting 的变体,其中收件人是类型为 ImpliedName 的上下文参数。

trait ImpliedGreeting(using val iname: ImpliedName):
  def msg = s"How are you, $iname"

case class ImpliedName(name: String):
  override def toString = name

trait ImpliedFormalGreeting extends ImpliedGreeting:
  override def msg = s"How do you do, $iname"

class F(using iname: ImpliedName) extends ImpliedFormalGreeting

最后一行中 F 的定义隐式扩展为

class F(using iname: ImpliedName) extends
  Object, // implicitly inserted
  ImpliedGreeting(using iname), // implicitly inserted
  ImpliedFormalGreeting

根据规则 4,F 也需要扩展 ImpliedGreeting 并向其传递参数,但请注意,由于 ImpliedGreeting 只有上下文参数,因此扩展是隐式添加的。

对象定义

TmplDef         ::=  ‘object’ ObjectDef
ObjectDef       ::=  id ClassTemplate

一个对象定义定义了一个新类的单个对象。它最通用的形式是 object ´m´ extends ´t´。这里,´m´ 是要定义的对象的名称,´t´ 是一个 模板,其形式为

´sc´ with ´mt_1´ with ... with ´mt_n´ { ´\mathit{stats}´ }

它定义了 ´m´ 的基类、行为和初始状态。extends 子句 extends ´sc´ with ´mt_1´ with ... with ´mt_n´ 可以省略,在这种情况下,假设 extends scala.AnyRef。类体 { ´\mathit{stats}´ } 也可以省略,在这种情况下,假设空体 {}

对象定义定义了一个符合模板 ´t´ 的单个对象(或:模块)。它大致等效于以下延迟值的定义

lazy val ´m´ = new ´sc´ with ´mt_1´ with ... with ´mt_n´ { this: ´m.type´ => ´\mathit{stats}´ }

请注意,由对象定义定义的值是延迟实例化的。new ´m´$cls 构造函数不是在对象定义点进行评估,而是在程序执行期间第一次取消引用 ´m´ 时进行评估(可能永远不会)。在构造函数评估期间再次尝试取消引用 ´m´ 将导致无限循环或运行时错误。其他线程在构造函数正在评估时尝试取消引用 ´m´ 将阻塞,直到评估完成。

上面给出的扩展对于顶级对象并不准确。它不可能,因为变量和方法定义不能出现在 包对象 之外的顶级。

示例

Scala 中的类没有静态成员;但是,可以通过伴随对象定义来实现等效的效果。例如

abstract class Point {
  val x: Double
  val y: Double
  def isOrigin = (x == 0.0 && y == 0.0)
}
object Point {
  val origin = new Point() { val x = 0.0; val y = 0.0 }
}

这定义了一个类 Point 和一个对象 Point,其中包含 origin 作为成员。请注意,Point 的双重使用是合法的,因为类定义在类型名称空间中定义了名称 Point,而对象定义在术语名称空间中定义了名称。

当 Scala 编译器解释包含静态成员的 Java 类时,会应用此技术。这样的类 ´C´ 从概念上看,是一个包含 ´C´ 所有实例成员的 Scala 类和一个包含 ´C´ 所有静态成员的 Scala 对象的组合。

通常,类的 伴生模块 是一个与类同名且在相同作用域和编译单元中定义的对象。反之,该类被称为模块的 伴生类

与具体类定义非常类似,对象定义仍然可以包含抽象类型成员的定义,但不能包含抽象项成员的定义。

枚举定义

TmplDef   ::=  ‘enum’ EnumDef
EnumDef   ::=  id ClassConstr [‘extends’ ConstrApps] EnumBody
EnumBody  ::=  [nl] ‘{’ [SelfType] EnumStat {semi EnumStat} ‘}’
EnumStat  ::=  TemplateStat
            |  {Annotation [nl]} {Modifier} EnumCase
EnumCase  ::=  ‘case’ (id ClassConstr [‘extends’ ConstrApps] | ids)

枚举定义 意味着定义一个 枚举类、一个伴生对象和一个或多个 枚举情况

枚举定义对于编码广义代数数据类型和枚举类型都很有用。

编译器将枚举定义扩展为仅使用 Scala 其他语言特性的代码。因此,Scala 中的枚举定义是方便的 语法糖,但它们对于理解 Scala 的核心并不重要。

现在我们详细解释枚举定义的扩展。首先,一些术语和符号约定

示例

Planet 枚举的一个示例枚举可以给出如下

enum Planet(mass: Double, radius: Double):
  case Mercury extends Planet(3.303e+23, 2.4397e6)
  case Venus   extends Planet(4.869e+24, 6.0518e6)
  case Earth   extends Planet(5.976e+24, 6.37814e6)
  case Mars    extends Planet(6.421e+23, 3.3972e6)
  case Jupiter extends Planet(1.9e+27,   7.1492e7)
  case Saturn  extends Planet(5.688e+26, 6.0268e7)
  case Uranus  extends Planet(8.686e+25, 2.5559e7)
  case Neptune extends Planet(1.024e+26, 2.4746e7)

  private inline val G = 6.67300E-11
  def surfaceGravity = G * mass / (radius * radius)
  def surfaceWeight(otherMass: Double) = otherMass * surfaceGravity
end Planet
示例

Option ADT 的一个示例枚举可以给出如下

enum Option[+T]:
  case Some(x: T)
  case None

枚举定义的降低

摘要

枚举类表示为扩展scala.reflect.Enum特性的sealed abstract类。

枚举情况表示如下

精确规则

scala.reflect.Enum特质定义了一个公共方法ordinal

package scala.reflect

transparent trait Enum extends Any, Product, Serializable:

  def ordinal: Int

有九个反糖化规则。规则 (1) 反糖化枚举定义。规则 (2) 反糖化逗号分隔名称的枚举情况为简单枚举情况。规则 (3) 到 (7) 反糖化枚举情况的可推断细节。规则 (8) 和 (9) 定义了完全反糖化的枚举情况如何映射到case classes 或 vals。在以下情况下,必须提供显式的extends子句,其中规则 (2) 到 (6) 不适用

  1. enum定义

    enum ´E´ <type-params> <value-params> extends <parents> { <defs> <cases> }
    

    扩展为一个扩展scala.reflect.Enum特质的sealed abstract类,以及一个包含定义情况的关联伴生对象,根据规则 (2 - 8) 扩展。枚举类以编译器生成的导入开头,该导入导入所有情况的名称<caseIds>,以便它们可以在类中无前缀使用。

    sealed abstract class ´E´ <type-params> <value-params>
        extends <parents> with scala.reflect.Enum {
      import ´E´.{ <caseIds> }
      <defs>
    }
    object ´E´ { <cases> }
    
  2. 由逗号分隔的名称列表组成的简单枚举情况

    case ´C_1´, ..., ´C_n´
    

    扩展为以下简单枚举情况

    case ´C_1´; ...; case ´C_n´
    

    原始情况上的任何修饰符或注释都扩展到所有扩展情况。

    然后,此结果将通过 (3 或 4) 进一步重写。

  3. 非参数化枚举´E´的简单枚举情况´C´,没有类型参数

    case ´C´
    

    扩展为以下值枚举情况

    case ´C´ extends ´E´
    

    然后,此结果将通过规则 (8) 进一步重写。

  4. 非参数化枚举´E´[´\mathit{tps}´]的简单枚举情况´C´,具有类型参数

    case ´C´
    

    其中´\mathit{tps}´ 具有以下形式

    ´\mathit{v}_1´ ´T_1´ >: ´L_1´ <: ´U_1´ ,   ... ,   ´\mathit{v}_n´ ´T_n´ >: ´L_n´ <: ´U_n´      (n > 0)
    

    其中每个方差 ´\mathit{v}_i´ 都是 '+''-',扩展到以下值枚举情况

    case ´C´ extends ´E´[´B_1´, ..., ´B_n´]
    

    其中 ´B_i´ 如果 ´\mathit{v}_i´ = '+' 则为 ´L_i´,如果 ´\mathit{v}_i´ = '-' 则为 ´U_i´

    然后,此结果将通过规则 (8) 进一步重写。

  5. 具有类型参数但没有 extends 子句的类枚举情况

    case ´C´[´\mathit{tps}´](´\mathit{ps}_1\,´)...(´\mathit{ps}_n´)
    

    未参数化的枚举 ´E´(没有类型参数)扩展到

    case ´C´[´\mathit{tps}´](´\mathit{ps}_1\,´)...(´\mathit{ps}_n´) extends ´E´
    

    然后使用规则 (9) 进一步重写此结果。

  6. 没有类型参数或 extends 子句的类枚举情况

    case ´C´(´\mathit{ps}_1\,´)...(´\mathit{ps}_n´)
    

    未参数化的枚举 ´E´[´\mathit{tps}´](具有类型参数)扩展到

    case ´C´(´\mathit{ps}_1\,´)...(´\mathit{ps}_n´) extends ´E´[´\mathit{tps}´]
    

    然后使用规则 (7) 进一步重写此结果。

  7. 没有类型参数但具有 extends 子句的类枚举情况

    case ´C´(´\mathit{ps}_1\,´)...(´\mathit{ps}_n´) extends <parents>
    

    枚举 ´E´[´\mathit{tps}´](具有类型参数)扩展到

    case ´C´[´\mathit{tps}´](´\mathit{ps}_1\,´)...(´\mathit{ps}_n´) extends <parents>
    

    前提是至少一个参数 ´\mathit{tps}´(´\mathit{ps}_1\,´)...(´\mathit{ps}_n´) 中的参数类型或 <parents> 中的类型参数中被提及。

    然后使用规则 (9) 进一步重写此结果。

  8. 单例枚举情况

    case ´C´ extends <parents>
    

    扩展到 ´E´ 的伴生对象中的以下 val 定义

    val ´C´ = $factory(_$ordinal = ´\mathit{n}´, $name = "C")
    

    其中 ´\mathit{n}´ 是伴生对象中情况的序数,从 0 开始。$factory 是一个占位符,它将它的参数扩展成一个表达式,该表达式产生与以下(可能共享的)匿名类的新的实例等效的东西

    new <parents> {
      def ordinal: Int = _$ordinal
      override def toString: String = $name
    }
    

    匿名类还实现了它从 Enum 继承的抽象 Product 方法。

    注意:如果值情况在 <parents> 中的类型参数中引用了 ´E´ 的类型参数,则这是一个错误。

  9. 类枚举情况

    case ´C´ <type-params> <value-params> extends <parents>
    

    类似于 ´E´ 的伴生对象中的最终情况类扩展

    final case class ´C´ <type-params> <value-params> extends <parents> {
      def ordinal = ´\mathit{n}´
    }
    

    其中 ´\mathit{n}´ 是伴生对象中情况的序数,从 0 开始。

    注意:如果类情况在 <type-params><value-params> 中的参数类型或 <parents> 的类型参数中引用了 ´E´ 的类型参数,则这是一个错误,除非该参数已经是情况的类型参数,即参数名称在 <type-params> 中定义。

枚举情况的超类

具有显式 extends 子句的枚举情况(单例或类)

case ´C´ <type-params> <value-params> extends <parents>

必须将父枚举 ´E´ 作为 <parents> 的第一个父级。

示例

考虑枚举 RGB,它由简单的枚举情况组成

enum RGB:
  case Red, Green, Blue

三个简单情况将在 RGB 的伴随对象中扩展如下

val Red = $new(0, "Red")
val Green = $new(1, "Green")
val Blue = $new(2, "Blue")

private def $new(_$ordinal: Int, $name: String) =
  new RGB with scala.runtime.EnumValue:
    def ordinal = _$ordinal
    override def productPrefix = $name
    override def toString = $name

枚举情况在构造后的扩展

类枚举情况的编译器生成的 applycopy 方法

case ´C´[´\mathit{tps}\,´](´\mathit{ps}_1\,´)...(´\mathit{ps}_n´) extends ´P_1´, ..., ´P_n´

被特殊对待。apply 方法的调用 ´C´[´\mathit{tps}\,´](´\mathit{ps}_1\,´)...(´\mathit{ps}_n´) 被赋予基础类型 ´P_1´ & ... & ´P_n´(删除任何 透明特征),只要该类型仍然与应用点的预期类型兼容。´C´copy 方法的调用 t.copy[´\mathit{tps}\,´](´\mathit{ps}_1\,´)...(´\mathit{ps}_n´) 以相同的方式处理。

仅包含单例情况的枚举的翻译

枚举 ´E´(可能是泛型的)定义了一个或多个单例情况,并且没有类情况将在其伴随对象中定义以下额外的合成成员(其中 ´E'´ 表示用通配符替换了任何类型参数的 ´E´

与 Java 兼容的枚举的翻译

与 Java 兼容的枚举是扩展 java.lang.Enum 的枚举。翻译规则与上述相同,但保留了本节中定义的保留项。

枚举情况的范围

enum 中的案例类似于辅助构造函数。它既不能使用 this 访问封闭的 enum,也不能使用简单标识符访问其值参数或实例成员。

即使翻译后的枚举情况位于枚举的伴随对象中,通过 this 或简单标识符引用此对象或其成员也是非法的。编译器在封闭伴随对象的范围内对枚举情况进行类型检查,但会将任何此类非法访问标记为错误。

类型参数的方差

具有推断类型参数的枚举 ´E´ 的参数化枚举情况 ´C´ 将复制方差注释。例如,来自 ´E´ 的类型参数 ´T_{i}´ 将具有与 ´C´ 中的类型参数 ´T'_{i}´ 相同的方差。

示例

以下枚举 View 具有一个逆变类型参数 ´T´ 和一个单例情况 Refl,它表示一个将类型 ´T´ 映射到自身的函数

enum View[-´T´]:
  case Refl(f: ´T´ => ´T´)

Refl 展开为以下枚举

enum View[-´T´]:
  case Refl[-´T'´](f: ´T'´ => ´T'´) extends View[´T'´]

Refl 的定义类型错误,因为它在函数类型的协变结果位置使用了逆变类型 ´T'´

一个类型正确的版本将在 Refl 案例中使用一个显式的、不变的类型参数 ´R´

enum View[-´T´]:
  case Refl[´R´](f: ´R´ => ´R´) extends View[´R´]

  1. Kennedy, Pierce. On Decidability of Nominal Subtyping with Variance. 在 FOOL 2007