类和对象
模板
模板定义了对象或单个对象的特征、行为和初始状态。模板是实例创建表达式、类定义和对象定义的一部分。模板 ´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´,而不会为其引入别名。
示例
考虑以下类定义
在这种情况下,O
的定义扩展为
从 Java 类型继承
模板可以具有 Java 类作为其超类,以及 Java 接口作为其混合。
模板评估
考虑一个模板 ´sc´ with ´mt_1´ with ´mt_n´ { ´\mathit{stats}´ }
。
如果这是一个 特质 的模板,那么它的混合评估包含对语句序列 ´\mathit{stats}´ 的评估。
如果这不是一个特质的模板,那么它的评估包含以下步骤。
- 首先,超类构造函数 ´sc´ 被 评估。
- 然后,模板中 线性化 中的所有基类,直到由 ´sc´ 表示的模板超类,都会被评估。评估按线性化中出现的顺序反向进行。每个评估如下进行
- 首先,从左到右评估 ´mt_i´ 的参数,并将其设置为 ´mt_i´ 的参数。
- 然后,对 ´mt_i´ 进行混合评估。
- 最后,评估语句序列 ´\mathit{stats}\,´。
构造函数调用
构造函数调用定义了由实例创建表达式创建的对象的类型、成员和初始状态,或者定义了类或对象定义继承的某个对象定义部分。构造函数调用是一个方法应用 ´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´)
的评估包含以下步骤
- 首先,评估前缀 ´x´。
- 然后,从左到右评估参数 ´\mathit{args}_1, ..., \mathit{args}_n´。
- 最后,正在构建的类通过评估由 ´c´ 引用的类的模板来初始化。
类线性化
通过从类 ´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} $$
示例
考虑以下类定义。
然后类 Iter
的线性化为
注意,类的线性化细化了继承关系:如果 ´C´ 是 ´D´ 的子类,则 ´C´ 在任何包含 ´C´ 和 ´D´ 的线性化中都位于 ´D´ 之前。 线性化 还满足这样的性质,即类的线性化始终包含其直接超类的线性化作为后缀。
例如,StringIterator
的线性化为
它是其子类 Iter
的线性化的后缀。对于混合类的线性化,情况并非如此。例如,RichIterator
的线性化为
它不是 Iter
的线性化的后缀。
类成员
由模板 ´C_1´ with ... with ´C_n´ { ´\mathit{stats}´ }
定义的类 ´C´ 可以在其语句序列 ´\mathit{stats}´ 中定义成员,并可以从所有父类继承成员。Scala 采用 Java 和 C# 的静态方法重载约定。因此,一个类可能定义和/或继承多个具有相同名称的方法。为了确定类 ´C´ 的定义成员是否覆盖了父类的成员,或者两者是否在 ´C´ 中作为重载变体共存,Scala 使用以下关于成员匹配的定义
定义:匹配
如果 ´M´ 和 ´M'´ 绑定相同的名称,并且以下之一成立,则成员定义 ´M´ 匹配成员定义 ´M'´。
- ´M´ 和 ´M'´ 都不是方法定义。
- ´M´ 和 ´M'´ 都定义了具有等效参数类型的单态方法。
- ´M´ 在 Java 中定义,并定义了一个具有空参数列表
()
的方法,而 ´M'´ 定义了一个无参数方法。 - ´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'´。
如果模板直接定义了两个匹配的成员,则会发生错误。如果模板包含两个具有相同名称和相同擦除类型的成员(直接定义或继承),也会发生错误。最后,模板不允许包含两个具有相同名称的方法(直接定义或继承),这两个方法都定义了默认参数。
示例
考虑以下特征定义
然后特征 D
具有直接定义的抽象成员 h
。它从特征 C
继承成员 f
,从特征 B
继承成员 g
。
覆盖
类 ´C´ 的成员 ´M´ 与 ´C´ 基类的非私有成员 ´M'´匹配,则称该成员覆盖该成员。在这种情况下,覆盖成员 ´M´ 的绑定必须包含被覆盖成员 ´M'´ 的绑定。此外,以下关于修饰符的限制适用于 ´M´ 和 ´M'´
- ´M'´ 不能是类。
- ´M'´ 不能标记为
final
。 - ´M´ 不能是
private
。 - 如果 ´M´ 被标记为
private[´C´]
用于某个封闭类或包 ´C´,则 ´M'´ 必须被标记为private[´C'´]
用于某个类或包 ´C'´,其中 ´C'´ 等于 ´C´ 或 ´C'´ 包含在 ´C´ 中。
- 如果 ´M´ 被标记为
protected
,那么 ´M'´ 也必须被标记为protected
。 - 如果 ´M'´ 不是抽象成员,那么 ´M´ 必须被标记为
override
。此外,以下两种情况之一必须成立:- 要么 ´M´ 定义在 ´M'´ 所定义的类的子类中,
- 要么 ´M´ 和 ´M'´ 都重写了第三个成员 ´M''´,该成员定义在包含 ´M´ 和 ´M'´ 的两个类的基类中。
- 如果 ´M'´ 在 ´C´ 中是 不完整的,那么 ´M´ 必须被标记为
abstract override
。 如果 ´M´ 和 ´M'´ 都是具体的数值定义,那么要么它们都没有标记为
lazy
,要么它们都必须标记为lazy
。稳定的成员只能被稳定的成员重写。例如,以下情况是不允许的:
另一个限制适用于抽象类型成员:具有 易变类型 作为其上限的抽象类型成员不能重写没有易变上限的抽象类型成员。
一个特殊规则涉及无参数方法。如果一个定义为 def ´f´: ´T´ = ...
或 def ´f´ = ...
的无参数方法重写了在 Java 中定义的类型为 ´()T'´ 的方法,该方法具有空参数列表,那么 ´f´ 也被认为具有空参数列表。
重写方法从超类中的定义继承所有默认参数。通过在重写方法中指定默认参数,可以添加新的默认值(如果超类中的对应参数没有默认值)或覆盖超类的默认值(否则)。
示例
考虑以下定义:
那么类定义 C
就不完善,因为 T
在 C
中的绑定是 type T <: B
,它不能包含 T
在类型 A
中的绑定 type T <: A
。可以通过在类 C
中添加 T
类型的重写定义来解决这个问题。
继承闭包
设 ´C´ 为一个类类型。´C´ 的继承闭包是最小的类型集 ´\mathscr{S}´,满足以下条件:
- ´C´ 属于 ´\mathscr{S}´。
- 如果 ´T´ 属于 ´\mathscr{S}´,那么构成 ´T´ 语法部分的每个类型 ´T'´ 也属于 ´\mathscr{S}´。
- 如果 ´T´ 是 ´\mathscr{S}´ 中的类类型,那么 ´T´ 的所有 父类 也属于 ´\mathscr{S}´。
如果类类型的继承闭包包含无限多个类型,则为静态错误。(此限制对于使子类型化可判定是必要的1)。
修饰符
成员定义之前可以添加修饰符,这些修饰符会影响由它们绑定的标识符的可访问性和使用方式。如果给出多个修饰符,它们的顺序无关紧要,但同一个修饰符不能出现多次。在重复定义之前出现的修饰符适用于所有组成定义。控制修饰符有效性和含义的规则如下。
private
private
修饰符可用于模板中的任何定义。模板的私有成员只能从直接封闭模板及其伴随模块或 伴随类 中访问。
private
修饰符对于 顶层 模板也是有效的。
private
修饰符可以用标识符 ´C´(例如 private[´C´]
)进行限定,该标识符必须表示封闭定义的类或包。用此类修饰符标记的成员分别只能从包 ´C´ 内部的代码或只能从类 ´C´ 内部的代码及其 伴随模块 中访问。
限定的另一种形式是 private[this]
。用此修饰符标记的成员 ´M´ 称为对象保护;它只能从定义它的对象内部访问。也就是说,选择 ´p.M´ 只有在前缀为 this
或 ´O´.this
时才合法,其中 ´O´ 是封闭引用的某个类。此外,还适用未限定 private
的限制。
标记为私有且没有限定符的成员称为类私有,而标记为 private[this]
的成员称为对象私有。如果成员是类私有或对象私有,则该成员是私有的,但如果它标记为 private[´C´]
其中 ´C´ 是一个标识符,则不是;在后一种情况下,该成员称为限定私有。
类私有或对象私有成员不能是抽象的,也不能具有 protected
或 override
修饰符。它们不会被子类继承,也不能覆盖父类中的定义。
protected
protected
修饰符适用于类成员定义。类的受保护成员可以从以下位置访问
- 定义类的模板,
- 所有将定义类作为基类的模板,
- 任何这些类的伴随模块。
protected
修饰符可以用标识符 ´C´(例如 protected[´C´]
)进行限定,该标识符必须表示封闭定义的类或包。用此类修饰符标记的成员也可以分别从包 ´C´ 内部的所有代码或从类 ´C´ 内部的所有代码及其 伴随模块 中访问。
受保护的标识符 ´x´ 只能在以下情况之一适用时用作选择 ´r´.´x´
中的成员名称
- 访问在定义成员的模板内,或者,如果给出限定 ´C´,则在包 ´C´ 内,或类 ´C´ 内,或其伴随模块内,或
- ´r´ 是保留字
this
和super
之一,或者 - ´r´ 的类型符合包含访问的类的类型实例。
另一种限定形式是 protected[this]
。用此修饰符标记的成员 ´M´ 称为对象保护;它只能从定义它的对象内部访问。也就是说,选择 ´p.M´ 只有在前缀是 this
或 ´O´.this
时才合法,其中 ´O´ 是包含该引用的某个类。此外,还适用非限定 protected
的限制。
覆盖
override
修饰符适用于类成员定义。对于在父类中覆盖其他具体成员定义的成员定义,它是强制性的。如果给出了 override
修饰符,则必须至少有一个被覆盖的成员定义(具体或抽象)。
抽象覆盖
override
修饰符与 abstract
修饰符结合使用时具有额外的意义。该修饰符组合仅允许用于特性的值成员。
如果模板的成员 ´M´ 是抽象的,或者它被标记为 abstract
和 override
并且 ´M´ 覆盖的每个成员再次不完整,则我们称该成员 ´M´ 为不完整。
请注意,abstract override
修饰符组合不会影响成员是具体还是抽象的概念。
抽象
abstract
修饰符用于类定义。对于特性,它是多余的,对于所有具有不完整成员的其他类,它是强制性的。抽象类不能用构造函数调用实例化,除非后面跟着混合和/或细化,这些混合和/或细化覆盖了类的所有不完整成员。只有抽象类和特性可以具有抽象术语成员。
abstract
修饰符也可以与 override
结合使用,用于类成员定义。在这种情况下,前面讨论的内容适用。
最终
final
修饰符适用于类成员定义和类定义。final
类成员定义不能在子类中被覆盖。final
类不能被模板继承。final
对对象定义是多余的。最终类或对象的成员隐式地也是最终的,因此 final
修饰符通常对它们也是多余的。但是请注意,常量值定义 确实需要显式的 final
修饰符,即使它们是在最终类或对象中定义的。final
允许用于抽象类,但不能应用于特性或不完整成员,也不能在一个修饰符列表中与 sealed
结合使用。
密封
sealed
修饰符应用于类定义。sealed
类不能直接继承,除非继承模板定义在与继承类相同的源文件中。但是,密封类的子类可以在任何地方继承。
延迟
lazy
修饰符应用于值定义。lazy
值在第一次访问时初始化(可能根本不会发生)。在初始化期间尝试访问延迟值可能会导致循环行为。如果在初始化期间抛出异常,则该值被认为未初始化,并且以后的访问将尝试重新评估其右侧。
中缀
infix
修饰符应用于方法定义和类型定义。它表示该方法或类型旨在用于中缀位置,即使它具有字母数字名称。
如果一个方法覆盖了另一个方法,则它们的 infix
注释必须一致。要么都用 infix
注释,要么都不注释。
infix
方法的第一个非接收器参数列表必须定义正好一个参数。示例
infix
修饰符也可以用于具有正好两个类型参数的类型、特征或类定义。像
这样的中缀类型可以使用中缀语法应用,即 A op B
。
示例
以下代码说明了限定私有的使用
这里,对方法 f
的访问可以在 Outer
中的任何地方出现,但在其外部则不能。对方法 g
的访问可以在包 outerpkg.innerpkg
中的任何地方出现,就像 Java 中的包私有方法一样。最后,对方法 h
的访问可以在包 outerpkg
中的任何地方出现,包括它包含的包。
示例
一个有用的习惯用法来阻止类的客户端构造该类的新的实例是声明该类为 abstract
和 sealed
例如,在上面的代码中,客户端只能通过调用现有 m.C
对象的 nextC
方法来创建类 m.C
的实例;客户端无法直接创建类 m.C
的对象。事实上,以下两行都有错误
可以通过将主构造函数标记为 private
来实现类似的访问限制(示例)。
类定义
类定义最通用的形式是
这里,
- ´c´ 是要定义的类的名称。
- ´\mathit{tps}´ 是正在定义的类的类型参数的非空列表。类型参数的作用域是整个类定义,包括类型参数部分本身。定义两个具有相同名称的类型参数是非法的。类型参数部分
[´\mathit{tps}\,´]
可以省略。具有类型参数部分的类称为多态类,否则称为单态类。 - ´as´ 是可能为空的 注释 序列。如果给出了任何注释,它们将应用于类的主要构造函数。
- ´m´ 是一个 访问修饰符,例如
private
或protected
,可能带有限定词。如果给出了这样的访问修饰符,它将应用于类的主要构造函数。 ´(\mathit{ps}_1)...(\mathit{ps}_n)´ 是类主要构造函数的形式值参数子句。形式值参数的作用域包括所有后续参数部分和模板 ´t´。但是,形式值参数不能构成类模板 ´t´ 的任何父类或成员的类型的一部分。定义两个具有相同名称的形式值参数是非法的。
如果一个类没有非隐式的形式参数部分,则假设为空参数部分
()
。如果形式参数定义 ´x: T´ 前面有
val
或var
关键字,则会隐式地向类添加此参数的访问器 定义。访问器引入类 ´c´ 的一个值成员 ´x´,该成员被定义为参数的别名。如果引入关键字是
var
,则还会隐式地向类添加一个设置器访问器´x´_=
。对该设置器的调用´x´_=(´e´)
会将参数的值更改为评估 ´e´ 的结果。形式参数定义可能包含修饰符,这些修饰符将传递到访问器定义中。当为参数给出访问修饰符但没有
val
或var
关键字时,将假设为val
。以val
或var
为前缀的形式参数不能同时是 按名称传递的参数。´t´ 是一个 模板,其形式为
它定义了类的基类、行为和对象的初始状态。
extends ´sc´ with ´mt_1´ with ... with ´mt_m´
扩展子句可以省略,在这种情况下,假设为extends scala.AnyRef
。类体{ ´\mathit{stats}´ }
也可以省略,在这种情况下,假设为空体{}
。
此类定义定义了一个类型 ´c´[´\mathit{tps}\,´]
和一个构造函数,当应用于符合类型 ´\mathit{ps}´ 的参数时,通过评估模板 ´t´ 初始化类型 ´c´[´\mathit{tps}\,´]
的实例。
示例 - val
和 var
参数
以下示例说明了类 C
的 val
和 var
参数
示例 - 私有构造函数
以下类只能从其伴生模块创建。
构造函数定义
除了主构造函数之外,类可能还有其他构造函数。这些由形式为 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´ 的构造函数调用,包括构造函数表达式本身中的自构造函数调用,都适用 重载解析 的通常规则。但是,与其他方法不同,构造函数永远不会被继承。为了防止构造函数调用无限循环,有一个限制,即每个自构造函数调用必须引用一个在其之前的构造函数定义(即它必须引用前面的辅助构造函数或类的主构造函数)。
示例
考虑以下类定义
这定义了一个名为 LinkedList
的类,它有三个构造函数。第二个构造函数构造一个单例列表,而第三个构造函数构造一个具有给定头和尾的列表。
案例类
如果类定义以 case
为前缀,则该类被称为案例类。
案例类必须有一个非隐式的参数部分。第一个参数部分中的形式参数被称为元素,它们被特殊对待。首先,可以通过构造函数模式将此类参数的值提取为字段。其次,除非参数已经带有 val
或 var
修饰符,否则会隐式地向此类参数添加 val
前缀。因此,将为参数 生成 一个访问器定义。
´c´[´\mathit{tps}\,´](´\mathit{ps}_1\,´)...(´\mathit{ps}_n´)
的案例类定义,其中 ´\mathit{tps}´ 是类型参数,´\mathit{ps}´ 是值参数,意味着定义了一个伴生对象,它充当 提取器对象。它具有以下形状
这里,´\mathit{Ts}´ 代表类型参数部分 ´\mathit{tps}´ 中定义的类型向量,每个 ´\mathit{xs}_i´ 表示参数部分 ´\mathit{ps}_i´ 的参数名称,´\mathit{xs}_{11}, ... , \mathit{xs}_{1k}´ 表示第一个参数部分 ´\mathit{xs}_1´ 中所有参数的名称。如果类中缺少类型参数部分,则 apply
和 unapply
方法中也会缺少类型参数部分。
如果伴生对象 ´c´ 已经定义,则 apply
和 unapply
方法将被添加到现有对象中。如果对象 ´c´ 已经具有 匹配 的 apply
(或 unapply
)成员,则不会添加新的定义。如果类 ´c´ 是 abstract
,则会省略 apply
的定义。
如果 case class 定义包含一个空的 value 参数列表,则 unapply
方法返回一个 Boolean
而不是 Option
类型,定义如下
如果 ´c´ 的第一个参数部分 ´\mathit{ps}_1´ 以一个 重复参数 结尾,则 unapply
方法的名称将更改为 unapplySeq
。
除非该类已经具有具有该名称的成员(直接定义或继承),或者该类具有重复参数,否则会隐式地向每个 case class 添加一个名为 copy
的方法。该方法定义如下
同样,´\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
的某些方法定义。特别是
- 方法
equals: (Any)Boolean
是结构相等,如果两个实例都属于所讨论的 case class 并且它们具有相等的(相对于equals
)构造函数参数(限制为类的元素,即第一个参数部分),则这两个实例相等。 - 方法
hashCode: Int
计算哈希码。如果数据结构成员的 hashCode 方法将相等(相对于 equals)的值映射到相等的哈希码,则 case class hashCode 方法也是如此。 - 方法
toString: String
返回一个字符串表示形式,其中包含类的名称及其元素。
示例
以下是 lambda 演算的抽象语法定义
这定义了一个类 Expr
,它具有 case class Var
、Apply
和 Lambda
。lambda 表达式的按值调用评估器可以写成如下。
可以在程序的其他部分定义扩展类型 Expr
的其他 case class,例如
可以通过声明基类 Expr
为 sealed
来排除这种可扩展性形式;在这种情况下,所有直接扩展 Expr
的类都必须与 Expr
位于同一个源文件中。
特征
特征是一个旨在作为混入添加到其他类的类。此外,不会将构造函数参数传递给特征的超类。这是不必要的,因为特征在超类初始化后进行初始化。
假设特征 ´D´ 定义了类型 ´C´ 的实例 ´x´ 的某些方面(即 ´D´ 是 ´C´ 的基类)。然后 ´D´ 在 ´x´ 中的实际超类型是由 ´\mathcal{L}(C)´ 中所有在 ´D´ 之后出现的基类组成的复合类型。实际超类型为在特征中解析 super
引用 提供了上下文。请注意,实际超类型取决于在混入组合中添加特征的类型;它在定义特征时不是静态已知的。
如果 ´D´ 不是特征,那么它的实际超类型就是它的最小真超类型(它是静态已知的)。
示例
以下特征定义了与某些类型的对象进行比较的属性。它包含一个抽象方法 <
和其他比较运算符 <=
、>
和 >=
的默认实现。
示例
考虑一个抽象类 Table
,它实现从键类型 A
到值类型 B
的映射。该类具有一个 set
方法,用于将新的键/值对输入表,以及一个 get
方法,用于返回与给定键匹配的可选值。最后,还有一个 apply
方法,它类似于 get
,但如果表对于给定键未定义,则返回给定的默认值。该类实现如下。
这是 Table
类的具体实现。
这是一个特征,它阻止对父类的 get
和 set
操作进行并发访问
请注意,SynchronizedTable
并没有向其超类 Table
传递参数,即使 Table
定义了形式参数。还要注意,SynchronizedTable
中 get
和 set
方法的 super
调用静态地引用了类 Table
中的抽象方法。只要调用方法标记为 abstract override
,这都是合法的。
最后,以下混合组合创建了一个同步列表表,其中字符串作为键,整数作为值,默认值为 0
对象 MyTable
从 SynchronizedTable
继承了 get
和 set
方法。这些方法中的 super
调用被重新绑定以引用 ListTable
中的相应实现,ListTable
是 MyTable
中 SynchronizedTable
的实际超类型。
扩展参数化特征
扩展带参数的特征需要额外的规则
如果类
´C´
扩展了参数化特征´T´
,而其超类没有,则´C´
必须 向´T´
传递参数。如果类
´C´
扩展了参数化特征´T´
,而其超类也扩展了,则´C´
不能 向´T´
传递参数。特征绝不能向父特征传递参数。
如果类
´C´
扩展了非参数化特征´T_i´
,而´T_i´
的基类型包含参数化特征´T_j´
,并且´C´
的超类没有扩展´T_j´
,那么´C´
必须 也显式扩展´T_j´
并传递参数。如果缺少的特征只包含上下文参数,则此规则将放宽。在这种情况下,特征引用将作为具有推断参数的附加父级隐式插入。
示例 - 防止歧义
以下列表尝试使用不同的参数两次扩展 Greeting
。
此程序应该打印 "Bob" 还是 "Bill"?事实上,这个程序是非法的,因为它违反了上面的规则 2。相反,D
可以扩展 Greeting
而不传递参数。
示例 - 覆盖
这是一个覆盖 msg
的 Greeting
变体
根据规则 4,扩展 FormalGreeting
的以下类需要也使用参数扩展 Greeting
示例 - 推断上下文参数
这是一个 Greeting
的变体,其中收件人是类型为 ImpliedName
的上下文参数。
最后一行中 F
的定义隐式扩展为
根据规则 4,F
也需要扩展 ImpliedGreeting
并向其传递参数,但请注意,由于 ImpliedGreeting
只有上下文参数,因此扩展是隐式添加的。
对象定义
一个对象定义定义了一个新类的单个对象。它最通用的形式是 object ´m´ extends ´t´
。这里,´m´ 是要定义的对象的名称,´t´ 是一个 模板,其形式为
它定义了 ´m´ 的基类、行为和初始状态。extends 子句 extends ´sc´ with ´mt_1´ with ... with ´mt_n´
可以省略,在这种情况下,假设 extends scala.AnyRef
。类体 { ´\mathit{stats}´ }
也可以省略,在这种情况下,假设空体 {}
。
对象定义定义了一个符合模板 ´t´ 的单个对象(或:模块)。它大致等效于以下延迟值的定义
请注意,由对象定义定义的值是延迟实例化的。new ´m´$cls
构造函数不是在对象定义点进行评估,而是在程序执行期间第一次取消引用 ´m´ 时进行评估(可能永远不会)。在构造函数评估期间再次尝试取消引用 ´m´ 将导致无限循环或运行时错误。其他线程在构造函数正在评估时尝试取消引用 ´m´ 将阻塞,直到评估完成。
上面给出的扩展对于顶级对象并不准确。它不可能,因为变量和方法定义不能出现在 包对象 之外的顶级。
示例
Scala 中的类没有静态成员;但是,可以通过伴随对象定义来实现等效的效果。例如
这定义了一个类 Point
和一个对象 Point
,其中包含 origin
作为成员。请注意,Point
的双重使用是合法的,因为类定义在类型名称空间中定义了名称 Point
,而对象定义在术语名称空间中定义了名称。
当 Scala 编译器解释包含静态成员的 Java 类时,会应用此技术。这样的类 ´C´ 从概念上看,是一个包含 ´C´ 所有实例成员的 Scala 类和一个包含 ´C´ 所有静态成员的 Scala 对象的组合。
通常,类的 伴生模块 是一个与类同名且在相同作用域和编译单元中定义的对象。反之,该类被称为模块的 伴生类。
与具体类定义非常类似,对象定义仍然可以包含抽象类型成员的定义,但不能包含抽象项成员的定义。
枚举定义
枚举定义 意味着定义一个 枚举类、一个伴生对象和一个或多个 枚举情况。
枚举定义对于编码广义代数数据类型和枚举类型都很有用。
编译器将枚举定义扩展为仅使用 Scala 其他语言特性的代码。因此,Scala 中的枚举定义是方便的 语法糖,但它们对于理解 Scala 的核心并不重要。
现在我们详细解释枚举定义的扩展。首先,一些术语和符号约定
- 我们使用 ´E´ 作为枚举定义的名称,使用 ´C´ 作为出现在 ´E´ 中的枚举情况的名称。
- 我们使用
<...>
表示在某些情况下可能为空的语法结构。例如,<value-params>
表示一个或多个参数列表(´\mathit{ps}_1\,´)...(´\mathit{ps}_n´)
或根本没有。 - 枚举类分为两类
- 参数化 枚举类至少有一个或多个(可能为空)项参数子句,表示为
(´\mathit{ps}_1\,´)...(´\mathit{ps}_n´)
。 - 非参数化 枚举类没有项参数子句,但可以选择包含类型参数子句,表示为
[´\mathit{tps}\,´]
。
- 参数化 枚举类至少有一个或多个(可能为空)项参数子句,表示为
枚举情况分为三类
- 类枚举情况 是那些可能包含类型参数子句
[´\mathit{tps}\,´]
,并且必须包含一个或多个(可能为空)参数子句(´\mathit{ps}_1\,´)...(´\mathit{ps}_n´)
的情况。 - 简单枚举情况 是那些没有参数子句和扩展子句的情况。也就是说,它们只包含一个名称。
- 值枚举情况 是那些没有参数子句但确实包含一个(可能生成的)
extends
子句的情况。
- 类枚举情况 是那些可能包含类型参数子句
简单枚举情况和值枚举情况统称为 单例枚举情况。
示例
Planet
枚举的一个示例枚举可以给出如下
示例
Option ADT 的一个示例枚举可以给出如下
枚举定义的降低
摘要
枚举类表示为扩展scala.reflect.Enum
特性的sealed abstract
类。
枚举情况表示如下
- 类枚举情况映射到枚举类伴生对象的
case class
成员, - 单例枚举情况映射到枚举类伴生对象的
val
成员,由本地类定义实现。该本地类是否与其他单例情况共享,以及与哪些共享,留作实现细节。
精确规则
scala.reflect.Enum
特质定义了一个公共方法ordinal
。
有九个反糖化规则。规则 (1) 反糖化枚举定义。规则 (2) 反糖化逗号分隔名称的枚举情况为简单枚举情况。规则 (3) 到 (7) 反糖化枚举情况的可推断细节。规则 (8) 和 (9) 定义了完全反糖化的枚举情况如何映射到case class
es 或 val
s。在以下情况下,必须提供显式的extends
子句,其中规则 (2) 到 (6) 不适用
- 任何参数化枚举的枚举情况,
- 任何非变体类型参数的非参数化枚举的单例枚举情况,
- 任何具有类型参数的枚举的类枚举情况,其中该情况也具有类型参数。
enum
定义扩展为一个扩展
scala.reflect.Enum
特质的sealed abstract
类,以及一个包含定义情况的关联伴生对象,根据规则 (2 - 8) 扩展。枚举类以编译器生成的导入开头,该导入导入所有情况的名称<caseIds>
,以便它们可以在类中无前缀使用。由逗号分隔的名称列表组成的简单枚举情况
扩展为以下简单枚举情况
原始情况上的任何修饰符或注释都扩展到所有扩展情况。
然后,此结果将通过 (3 或 4) 进一步重写。
非参数化枚举
´E´
的简单枚举情况´C´
,没有类型参数扩展为以下值枚举情况
然后,此结果将通过规则 (8) 进一步重写。
非参数化枚举
´E´[´\mathit{tps}´]
的简单枚举情况´C´
,具有类型参数其中
´\mathit{tps}´
具有以下形式其中每个方差
´\mathit{v}_i´
都是'+'
或'-'
,扩展到以下值枚举情况其中
´B_i´
如果´\mathit{v}_i´ = '+'
则为´L_i´
,如果´\mathit{v}_i´ = '-'
则为´U_i´
。然后,此结果将通过规则 (8) 进一步重写。
具有类型参数但没有 extends 子句的类枚举情况
未参数化的枚举
´E´
(没有类型参数)扩展到然后使用规则 (9) 进一步重写此结果。
没有类型参数或 extends 子句的类枚举情况
未参数化的枚举
´E´[´\mathit{tps}´]
(具有类型参数)扩展到然后使用规则 (7) 进一步重写此结果。
没有类型参数但具有 extends 子句的类枚举情况
枚举
´E´[´\mathit{tps}´]
(具有类型参数)扩展到前提是至少一个参数
´\mathit{tps}´
在(´\mathit{ps}_1\,´)...(´\mathit{ps}_n´)
中的参数类型或<parents>
中的类型参数中被提及。
然后使用规则 (9) 进一步重写此结果。单例枚举情况
扩展到
´E´
的伴生对象中的以下val
定义其中
´\mathit{n}´
是伴生对象中情况的序数,从 0 开始。$factory
是一个占位符,它将它的参数扩展成一个表达式,该表达式产生与以下(可能共享的)匿名类的新的实例等效的东西匿名类还实现了它从
Enum
继承的抽象Product
方法。
注意:如果值情况在<parents>
中的类型参数中引用了´E´
的类型参数,则这是一个错误。类枚举情况
类似于
´E´
的伴生对象中的最终情况类扩展其中
´\mathit{n}´
是伴生对象中情况的序数,从 0 开始。
注意:如果类情况在<type-params>
或<value-params>
中的参数类型或<parents>
的类型参数中引用了´E´
的类型参数,则这是一个错误,除非该参数已经是情况的类型参数,即参数名称在<type-params>
中定义。
枚举情况的超类
具有显式 extends 子句的枚举情况(单例或类)
必须将父枚举 ´E´
作为 <parents>
的第一个父级。
示例
考虑枚举 RGB
,它由简单的枚举情况组成
三个简单情况将在 RGB
的伴随对象中扩展如下
枚举情况在构造后的扩展
类枚举情况的编译器生成的 apply
和 copy
方法
被特殊对待。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´
)
- 方法
valueOf(name: String): ´E'´
。它返回标识符为name
的单例情况值。 - 方法
values
,它返回一个Array[´E'´]
,其中包含E
定义的所有单例情况值,按其定义顺序排列。
与 Java 兼容的枚举的翻译
与 Java 兼容的枚举是扩展 java.lang.Enum
的枚举。翻译规则与上述相同,但保留了本节中定义的保留项。
对于与 Java 兼容的枚举,具有类情况是一个编译时错误。
诸如
case C
之类的案例扩展为@static val
而不是val
。这允许它们作为枚举类型的静态字段生成,从而确保它们以与 Java 枚举相同的方式表示。
枚举情况的范围
enum
中的案例类似于辅助构造函数。它既不能使用 this
访问封闭的 enum
,也不能使用简单标识符访问其值参数或实例成员。
即使翻译后的枚举情况位于枚举的伴随对象中,通过 this
或简单标识符引用此对象或其成员也是非法的。编译器在封闭伴随对象的范围内对枚举情况进行类型检查,但会将任何此类非法访问标记为错误。
类型参数的方差
具有推断类型参数的枚举 ´E´
的参数化枚举情况 ´C´
将复制方差注释。例如,来自 ´E´
的类型参数 ´T_{i}´
将具有与 ´C´
中的类型参数 ´T'_{i}´
相同的方差。
示例
以下枚举 View
具有一个逆变类型参数 ´T´
和一个单例情况 Refl
,它表示一个将类型 ´T´
映射到自身的函数
Refl
展开为以下枚举
Refl
的定义类型错误,因为它在函数类型的协变结果位置使用了逆变类型 ´T'´
。
一个类型正确的版本将在 Refl
案例中使用一个显式的、不变的类型参数 ´R´
-
Kennedy, Pierce. On Decidability of Nominal Subtyping with Variance. 在 FOOL 2007 ↩