表达式
表达式由运算符和操作数组成。表达式形式将在后面按优先级递减的顺序讨论。
表达式类型
表达式的类型通常是相对于某个预期类型(可能未定义)的。当我们写“表达式 ´e´ 预期符合类型 ´T´”时,我们的意思是
- 表达式 ´e´ 的预期类型是 ´T´,并且
- 表达式 ´e´ 的类型必须符合 ´T´。
以下斯科伦化规则适用于每个表达式:如果表达式的类型是存在类型 ´T´,则该表达式的类型被假定为 ´T´ 的斯科伦化。
斯科伦化通过类型打包来逆转。假设一个类型为 ´T´ 的表达式 ´e´,并设 ´t_1[\mathit{tps}_1] >: L_1 <: U_1, ..., t_n[\mathit{tps}_n] >: L_n <: U_n´ 是 ´e´ 的某些部分的斯科伦化创建的所有在 ´T´ 中自由的类型变量。那么 ´e´ 的打包类型是
字面量
字面量的类型与其词法语法一起描述;它们的求值是立即的。
Null 值
null
值的类型是 scala.Null
,因此符合所有引用类型。它表示一个引用值,该值引用一个特殊的 null
对象。此对象在 scala.AnyRef
类中实现方法,如下所示
eq(´x\,´)
和==(´x\,´)
返回true
当且仅当参数 ´x´ 也是“null”对象。ne(´x\,´)
和!=(´x\,´)
返回 true 当且仅当参数 x 不是“null”对象。isInstanceOf[´T\,´]
始终返回false
。asInstanceOf[´T\,´]
返回类型 ´T´ 的 默认值。##
返回0
。
对“null”对象的任何其他成员的引用都会抛出 NullPointerException
。
标识符
标识符引用一个命名的项。它可以是简单名称或选择。
简单名称 ´x´ 引用 此处 指定的值。如果 ´x´ 被封闭类或对象 ´C´ 中的定义绑定,则它被认为等同于选择 ´C´.this.´x´
,其中 ´C´ 被认为引用包含 ´x´ 的类,即使类型名称 ´C´ 在 ´x´ 出现时被 遮蔽。
如果 ´r´ 是类型 ´T´ 的 稳定标识符,则选择 ´r.x´ 静态地引用 ´r´ 的一个项成员 ´m´,该成员在 ´T´ 中由名称 ´x´ 标识。
对于其他表达式 ´e´,´e.x´ 的类型与 { val ´y´ = ´e´; ´y´.´x´ }
相同,其中 ´y´ 是一个新的名称。
标识符前缀的预期类型始终未定义。标识符的类型是它引用的实体的类型 ´T´,但以下情况除外:在需要 稳定类型 的上下文中出现的 路径 ´p´ 的类型是单例类型 ´p´.type
。
需要稳定类型的上下文是满足以下条件之一的上下文
- 路径 ´p´ 作为选择的首部出现,并且它不指定常量,或者
- 预期类型 ´\mathit{pt}´ 是稳定类型,或者
- 预期类型 ´\mathit{pt}´ 是一个抽象类型,其下限是稳定类型,并且实体 ´p´ 引用的类型 ´T´ 不符合 ´\mathit{pt}´,或者
- 路径 ´p´ 指定一个模块。
选择 ´e.x´ 的评估方法是首先评估限定符表达式 ´e´,它会生成一个对象 ´r´。然后,选择的結果是 ´r´ 的成员,该成员要么由 ´m´ 定义,要么由覆盖 ´m´ 的定义定义。
This 和 Super
表达式 this
可以出现在模板或复合类型的语句部分。它代表着包含该引用的最内层模板或复合类型所定义的对象。如果这是一个复合类型,则 this
的类型就是该复合类型。如果它是一个类或对象的模板,其简单名称为 ´C´,则 this
的类型与 ´C´.this
的类型相同。
表达式 ´C´.this
在包含类或对象的语句部分是合法的,其简单名称为 ´C´。它代表着包含该引用的最内层定义所定义的对象。如果表达式的预期类型是稳定类型,或者 ´C´.this
作为选择的词缀出现,则其类型为 ´C´.this.type
,否则为类 ´C´ 的自身类型。
引用 super.´m´
静态地引用包含该引用的最内层模板的最小适当超类型中的方法或类型 ´m´。它评估为该模板的实际超类型中的成员 ´m'´,该超类型等于 ´m´ 或覆盖 ´m´。静态引用的成员 ´m´ 必须是类型或方法。
如果它是一个方法,它必须是具体的,或者包含该引用的模板必须有一个成员 ´m'´ 覆盖 ´m´ 并且标记为 abstract override
。
引用 ´C´.super.´m´
静态地引用包含该引用的最内层包含类或对象定义的最小适当超类型中的方法或类型 ´m´,该类或对象定义名为 ´C´。它评估为该类或对象的实际超类型中的成员 ´m'´,该超类型等于 ´m´ 或覆盖 ´m´。静态引用的成员 ´m´ 必须是类型或方法。如果静态引用的成员 ´m´ 是一个方法,它必须是具体的,或者名为 ´C´ 的最内层包含类或对象定义必须有一个成员 ´m'´ 覆盖 ´m´ 并且标记为 abstract override
。
super
前缀可以后跟特征限定符 [´T\,´]
,如 ´C´.super[´T\,´].´x´
。这被称为静态超引用。在这种情况下,引用的是 ´C´ 的父特征中 ´x´ 的类型或方法,其简单名称为 ´T´。该成员必须是唯一定义的。如果它是一个方法,它必须是具体的。
示例
考虑以下类定义
类 C
的线性化是 {C, B, Root}
,类 D
的线性化是 {D, B, A, Root}
。然后我们有
请注意,superB
方法返回不同的结果,具体取决于 B
是与类 Root
还是 A
混合在一起。
方法应用
应用 ´f(e_1, ..., e_m)´
将方法 ´f´
应用于参数表达式 ´e_1, ..., e_m´
。为了使此表达式类型正确,该方法必须适用于其参数
如果 ´f´ 的方法类型为 (´p_1´:´T_1´, ..., ´p_n´:´T_n´)´U´
,则每个参数表达式 ´e_i´ 的类型都与相应的参数类型 ´T_i´ 作为预期类型一致。令 ´S_i´ 为参数 ´e_i´ 的类型 ´(i = 1, ..., n)´。方法 ´f´ 必须对其类型为 ´S_1´, ..., ´S_n´ 的参数 ´e_1´, ..., ´e_n´ 适用。如果 ´f´ 的最后一个参数类型是 重复 的,则会尝试对与重复参数匹配的表达式列表的后缀 ´e_m´, ..., ´e_n´ 进行 协调。如果参数表达式 ´e_i´ 的形式为 ´x_i=e'_i´
且 ´x_i´
是参数名称 ´p_1´, ..., ´p_n´
中的一个,则称该参数表达式 ´e_i´ 为命名参数。
一旦确定了类型 ´S_i´,则如果满足以下所有条件,则上述方法类型的 ´f´ 方法就被认为是适用的
- 对于每个命名参数 ´p_j=e_i'´,类型 ´S_i´ 与参数类型 ´T_j´ 兼容;
- 对于每个位置参数 ´e_i´,类型 ´S_i´ 与 ´T_i´ 兼容;
- 如果定义了预期类型,则结果类型 ´U´ 与其 兼容。
如果 ´f´ 是某个值类型,则该应用被视为等效于 ´f´.apply(´e_1´, ..., ´e_m´)
,即 ´f´ 定义的 apply
方法的应用。如果 ´f´.apply
适用,则值 ´f´
适用于给定的参数。
注意
- 如果 ´f´ 或
´f´.apply
是多态方法,则将其视为 省略的类型应用。 - 如果此类型应用的结果适用,则
´f´
适用于给定的参数。
应用 ´f´(´e_1´, ..., ´e_n´)
会先计算 ´f´,然后从左到右计算每个参数 ´e_1´, ..., ´e_n´,但对应于按名称参数的参数除外(见下文)。每个参数表达式都将转换为其对应形式参数的类型。之后,应用将被重写为方法的右侧,实际参数替换形式参数。最后,将计算重写右侧的结果转换为方法声明的结果类型(如果有)。
具有无参数方法类型 => ´T´
的形式参数的情况会特殊处理。在这种情况下,相应的实际参数表达式 ´e´ 不会在应用之前求值。相反,在重写规则右侧使用形式参数时,都会重新求值 ´e´。换句话说,=>
参数的求值顺序是 按名调用,而普通参数的求值顺序是 按值调用。此外,要求 ´e´ 的 打包类型 符合参数类型 ´T´。如果由于命名参数或默认参数将应用转换为块,则按名参数的行为将保留。在这种情况下,该参数的局部值将具有 val ´y_i´ = () => ´e´
的形式,传递给方法的参数为 ´y_i´()
。
应用中的最后一个参数可以标记为序列参数,例如 ´e´: _*
。此类参数必须对应于类型为 ´S´*
的 重复参数,并且必须是匹配此参数的唯一参数(即形式参数和实际参数的数量必须相同)。此外,´e´ 的类型必须符合 scala.Seq[´T´]
,其中类型 ´T´ 符合 ´S´。在这种情况下,参数列表将通过用序列 ´e´ 的元素替换它来进行转换。当应用使用命名参数时,可变参数必须恰好指定一次。
如果只提供一个参数,则可以将其作为块表达式提供,并且可以省略括号,形式为 ´f´ { block }
。当 f
具有单个形式参数或所有其他形式参数具有默认值时,此方法有效。
方法应用通常会在程序的运行时堆栈上分配一个新的帧。但是,如果局部方法或最终方法将其自身作为其最后一个操作调用,则该调用将使用调用者的堆栈帧执行。
示例
假设以下方法计算可变数量参数的总和
那么
两者都将 10
作为结果。另一方面,
将无法类型检查。
参数列表可以以软关键字 using
开头,以方便与 Scala 3 进行交叉编译。该关键字将被忽略。
命名参数和默认参数
如果应用程序要使用命名参数 ´p = e´ 或默认参数,则必须满足以下条件。
- 对于每个出现在位置参数 ´e_1 ... e_m´ 左侧的命名参数 ´p_i = e_i´,参数位置 ´i´ 与参数 ´p_i´ 在应用方法的参数列表中的位置一致。
- 所有命名参数的名称 ´x_i´ 必须成对不同,并且任何命名参数都不能定义已由位置参数指定的参数。
- 每个未由位置参数或命名参数指定的正式参数 ´p_j:T_j´ 必须具有默认参数。
如果应用程序使用命名参数或默认参数,则将应用以下转换将其转换为不使用命名参数或默认参数的应用程序。
如果方法 ´f´ 的形式为 ´p.m´[´\mathit{targs}´]
,则将其转换为以下代码块:
如果方法 ´f´ 本身是一个应用程序表达式,则对 ´f´ 递归应用转换。转换 ´f´ 的结果是一个形式为以下代码块的代码块:
其中 ´(\mathit{args}_1), ..., (\mathit{args}_l)´ 中的每个参数都是对值 ´x_1, ..., x_k´ 中某一个值的引用。为了将当前应用程序集成到代码块中,首先为 ´e_1, ..., e_m´ 中的每个参数创建一个使用新名称 ´y_i´ 的值定义,对于位置参数,其初始化为 ´e_i´,对于形式为 ´x_i=e'_i´
的命名参数,其初始化为 ´e'_i´。然后,对于每个未由参数列表指定的参数,创建一个使用新名称 ´z_i´ 的值定义,其初始化使用计算该参数的 默认参数 的方法。
令 ´\mathit{args}´ 为生成的名称 ´y_i´ 和 ´z_i´ 的排列,使得每个名称的位置与其在方法类型 (´p_1:T_1, ..., p_n:T_n´)´U´
中对应参数的位置匹配。转换的最终结果是一个形式为以下代码块的代码块:
签名多态方法
对于目标平台的签名多态方法的调用 ´f´(´e_1, ..., e_m´)
,调用的方法在每个调用点具有不同的方法类型 (´p_1´:´T_1, ..., p_n´:´T_n´)´U´
。参数类型 ´T_, ..., T_n´
是参数表达式 ´e_1, ..., e_m´
的类型。如果签名多态方法的声明返回类型 ´R´
为除 scala.AnyRef
之外的任何类型,则返回类型 ´U´
为 ´R´
。否则,´U´
为调用点的预期类型。如果预期类型未定义,则 ´U´
为 scala.AnyRef
。参数名称 ´p_1, ..., p_n´
是新的。
注意
在 Java 平台版本 11 及更高版本上,签名多态方法是本地的,是 java.lang.invoke.MethodHandle
或 java.lang.invoke.VarHandle
的成员,并且具有单个类型为 java.lang.Object*
的重复参数。
方法值
表达式 ´e´ _
只有在 ´e´ 为方法类型或 ´e´ 为按名称传递参数时才格式良好。如果 ´e´ 是带参数的方法,则 ´e´ _
表示通过 eta 扩展 将 ´e´ 转换为函数类型。如果 ´e´ 是无参数方法或类型为 => ´T´
的按名称传递参数,则 ´e´ _
表示类型为 () => ´T´
的函数,该函数在应用于空参数列表 ()
时会计算 ´e´。
示例
左列中的方法值分别等效于右边的 eta 扩展表达式。
占位符语法 | eta 扩展 |
---|---|
math.sin _ |
x => math.sin(x) |
math.pow _ |
(x1, x2) => math.pow(x1, x2) |
val vs = 1 to 9; vs.fold _ |
(z) => (op) => vs.fold(z)(op) |
(1 to 9).fold(z)_ |
{ val eta1 = 1 to 9; val eta2 = z; op => eta1.fold(eta2)(op) } |
Some(1).fold(??? : Int)_ |
{ val eta1 = Some(1); val eta2 = () => ???; op => eta1.fold(eta2())(op) } |
请注意,方法名称和尾部下划线之间必须有一个空格,否则下划线将被视为名称的一部分。
类型应用
类型应用 ´e´[´T_1, ..., T_n´]
使用参数类型 ´T_1, ..., T_n´
实例化类型为 [´a_1´ >: ´L_1´ <: ´U_1, ..., a_n´ >: ´L_n´ <: ´U_n´]´S´
的多态方法 ´e´。每个参数类型 ´T_i´ 必须遵守相应的边界 ´L_i´ 和 ´U_i´。也就是说,对于每个 ´i = 1, ..., n´,我们必须有 ´\sigma L_i <: T_i <: \sigma U_i´,其中 ´\sigma´ 是替换 ´[a_1 := T_1, ..., a_n := T_n]´。应用的类型为 ´\sigma S´。
如果 ´e´ 不是方法,而是某种值类型,则类型应用被视为等效于 ´e´.apply[´T_1 , ...,´ T´_n´]
,即 ´e´ 定义的 apply
方法的应用。
如果 局部类型推断 可以从实际方法参数的类型和预期结果类型推断出多态方法的最佳类型参数,则可以省略类型应用。
元组
元组表达式 (´e_1´, ..., ´e_n´)
,其中 ´n \geq 2´,等效于表达式 ´e_1´ *: ... *: ´e_n´ *: scala.EmptyTuple
。
注意:由于对 *:
的调用速度很慢,因此可以实现更有效的翻译。例如,(´e_1´, ´e_2´)
可以翻译成 scala.Tuple2(´e_1´, ´e_2´)
,它实际上等效于 ´e_1´ *: ´e_2´ *: scala.EmptyTuple
。
注意
- 表达式
(´e_1´)
不等效于´e_1´ *: scala.EmptyTuple
,而是一个普通的带括号的表达式。 - 表达式
()
不是scala.EmptyTuple
的别名,而是类型scala.Unit
的唯一值。
实例创建表达式
简单实例创建表达式 形式为 new ´c´
,其中 ´c´ 是一个 构造函数调用。令 ´T´ 为 ´c´ 的类型。那么 ´T´ 必须表示 scala.AnyRef
的非抽象子类的(类型实例)。此外,表达式的具体自身类型必须符合由 ´T´ 表示的类的 自身类型。具体自身类型通常为 ´T´,除非表达式 new ´c´
作为值定义的右侧出现
(其中类型注释 : ´S´
可能缺失)。在后一种情况下,表达式的具体自身类型是复合类型 ´T´ with ´x´.type
。
通过创建类型为 ´T´ 的新对象来评估表达式,该对象通过评估 ´c´ 来初始化。表达式的类型为 ´T´。
通用实例创建表达式 形式为 new ´t´
,其中 ´t´ 是某个 类模板。这样的表达式等效于块
其中 ´a´ 是匿名类的新名称,用户程序无法访问。
还有一种创建结构类型值的简写形式:如果 {´D´}
是一个类体,那么 new {´D´}
等效于通用实例创建表达式 new AnyRef{´D´}
。
示例
考虑以下结构实例创建表达式
这是通用实例创建表达式 new AnyRef{´D´}
的简写形式。
后者又等效于块
其中 anon$X
是某个新创建的名称。
块
块表达式 {´s_1´; ...; ´s_n´; ´e\,´}
由一系列块语句 ´s_1, ..., s_n´ 和最终表达式 ´e´ 构成。语句序列不能包含在同一命名空间中绑定相同名称的两个定义。最终表达式可以省略,在这种情况下,将假定为单元值 ()
。
最终表达式 ´e´ 的预期类型是块的预期类型。所有前面语句的预期类型未定义。
块 ´s_1´; ...; ´s_n´; ´e´
的类型是某种类型 ´T´,使得
- ´U <: T´ 其中 ´U´ 是 ´e´ 的类型。
- 在 ´T´ 中没有值或类型名称是自由的,即 ´T´ 不引用在语句 ´s_1, ..., s_n´ 中的任何一个中局部定义的任何值或类型。
- ´T´ 是“尽可能小”(这是一个软性要求)。
我们计算 ´T´ 的精确方式,称为 *类型规避*,目前在此规范中未定义。
块的评估需要评估其语句序列,然后评估最终表达式 ´e´,它定义了块的结果。
块表达式 {´c_1´; ...; ´c_n´}
其中 ´c_1, ..., c_n´ 是 case 子句,构成一个 模式匹配匿名函数。
前缀、中缀和后缀运算
表达式可以由操作数和运算符构成。
前缀运算
前缀运算 ´\mathit{op};e´ 由一个前缀运算符 ´\mathit{op}´ 组成,它必须是标识符 ‘+
’,‘-
’,‘!
’ 或 ‘~
’ 之一,并且不能用反引号括起来。表达式 ´\mathit{op};e´ 等效于后缀方法应用 e.unary_´\mathit{op}´
。
前缀运算符不同于普通方法应用,因为它们的操作数表达式不必是原子的。例如,输入序列 -sin(x)
被读作 -(sin(x))
,而方法应用 negate sin(x)
将被解析为对操作数 negate
和 (x)
应用中缀运算符 sin
。
后缀运算
后缀运算符可以是任意标识符。后缀运算 ´e;\mathit{op}´ 被解释为 ´e.\mathit{op}´。
中缀运算
中缀运算符可以是任意标识符。中缀运算符的优先级和结合性定义如下
中缀运算符的 *优先级* 由运算符的第一个字符决定。字符按优先级递增顺序列出,同一行上的字符具有相同的优先级。
也就是说,以字母开头的运算符具有最低优先级,其次是以 ‘|
’ 开头的运算符,等等。
此规则有一个例外,涉及 赋值运算符。赋值运算符的优先级与简单赋值 (=)
的优先级相同。也就是说,它比任何其他运算符的优先级都低。
运算符的结合性由运算符的最后一个字符决定。以冒号“:
”结尾的运算符是右结合的。所有其他运算符都是左结合的。
运算符的优先级和结合性决定了表达式各个部分的组合方式。
- 如果表达式中存在多个中缀运算,则优先级较高的运算符比优先级较低的运算符更紧密地绑定。
- 如果存在连续的中缀运算 ´e_0; \mathit{op}_1; e_1; \mathit{op}_2 ... \mathit{op}_n; e_n´,其中运算符 ´\mathit{op}_1, ..., \mathit{op}_n´ 具有相同的优先级,则所有这些运算符必须具有相同的结合性。如果所有运算符都是左结合的,则该序列被解释为 ´(...(e_0;\mathit{op}_1;e_1);\mathit{op}_2...);\mathit{op}_n;e_n´。否则,如果所有运算符都是右结合的,则该序列被解释为 ´e_0;\mathit{op}_1;(e_1;\mathit{op}_2;(... \mathit{op}_n;e_n)...)´。
- 后缀运算符的优先级始终低于中缀运算符。例如,´e_1;\mathit{op}_1;e_2;\mathit{op}_2´ 始终等效于 ´(e_1;\mathit{op}_1;e_2);\mathit{op}_2´。
左结合运算符的右操作数可能包含用括号括起来的多个参数,例如 ´e;\mathit{op};(e_1,...,e_n)´。然后,此表达式被解释为 ´e.\mathit{op}(e_1,...,e_n)´。
左结合二元运算 ´e_1;\mathit{op};e_2´ 被解释为 ´e_1.\mathit{op}(e_2)´。如果 ´\mathit{op}´ 是右结合的,并且它的参数按名称传递,则相同的运算被解释为 ´e_2.\mathit{op}(e_1)´。如果 ´\mathit{op}´ 是右结合的,并且它的参数按值传递,则它被解释为 { val ´x´=´e_1´; ´e_2´.´\mathit{op}´(´x\,´) }
,其中 ´x´ 是一个新的名称。
在 -source:future
下,如果方法名称是字母数字,并且目标方法未标记为 infix
,则会发出弃用警告。
赋值运算符
赋值运算符是一个运算符符号(标识符 中的语法类别 op
),以等号“=
”结尾,以下情况除外
- 运算符也以等号开头,或者
- 运算符是
(<=)
、(>=)
、(!=)
之一。
赋值运算符的处理方式特殊,如果其他解释无效,它们可以扩展为赋值。
让我们考虑一个赋值运算符,例如 +=
在中缀运算 ´l´ += ´r´
中,其中 ´l´、´r´ 是表达式。此运算可以重新解释为对应于赋值的运算
除了运算的左侧 ´l´ 只被评估一次。
如果满足以下两个条件,则会发生重新解释。
- 左侧 ´l´ 没有名为
+=
的成员,也不能通过 隐式转换 转换为具有名为+=
的成员的值。 - 赋值
´l´ = ´l´ + ´r´
类型正确。特别是,这意味着 ´l´ 指向一个可以被赋值的变量或对象,并且可以转换为具有名为+
的成员的值。
类型化表达式
类型化表达式 ´e: T´ 的类型为 ´T´。表达式 ´e´ 的类型应符合 ´T´。表达式的结果是 ´e´ 的值转换为类型 ´T´。
示例
以下是一些类型正确和类型错误的表达式的示例。
带注释的表达式
带注释的表达式 ´e´: @´a_1´ ... @´a_n´
将 注释 ´a_1, ..., a_n´ 附加到表达式 ´e´。
赋值
对简单变量 ´x´ = ´e´
的赋值的解释取决于 ´x´ 的定义。如果 ´x´ 表示一个可变变量,则赋值会将 ´x´ 的当前值更改为表达式 ´e´ 的计算结果。´e´ 的类型应符合 ´x´ 的类型。如果 ´x´ 是在某个模板中定义的无参数方法,并且同一个模板包含一个 setter 方法 ´x´_=
作为成员,则赋值 ´x´ = ´e´
被解释为对该 setter 方法的调用 ´x´_=(´e\,´)
。类似地,对无参数方法 ´x´ 的赋值 ´f.x´ = ´e´
被解释为调用 ´f.x´_=(´e\,´)
。如果 ´x´ 是对一元运算符的应用,则表达式被解释为它被写成显式应用 ´x´.unary_´\mathit{op}´
,即 ´x´.unary_´\mathit{op}´_=(´e\,´)
。
带有方法应用在 ‘=
’ 运算符左侧的赋值 ´f´(´\mathit{args}\,´) = ´e´
被解释为 ´f.´update(´\mathit{args}´, ´e\,´)
,即对 ´f´ 定义的 update
方法的调用。
示例
以下是一些赋值表达式及其等效扩展。
赋值 | 扩展 |
---|---|
x.f = e |
x.f_=(e) |
x.f() = e |
x.f.update(e) |
x.f(i) = e |
x.f.update(i, e) |
x.f(i, j) = e |
x.f.update(i, j, e) |
矩阵乘法的示例命令式代码
以下是矩阵乘法的常用命令式代码。
对数组访问和赋值进行反糖化,得到以下扩展版本
条件表达式
条件表达式 if (´e_1´) ´e_2´ else ´e_3´
根据 ´e_1´ 的值选择 ´e_2´ 和 ´e_3´ 中的一个值。条件 ´e_1´ 预期符合类型 Boolean
。then 部分 ´e_2´ 和 else 部分 ´e_3´ 预期都符合条件表达式的预期类型。如果没有预期类型,则尝试对 ´e_2´ 和 ´e_3´ 进行协调。条件表达式的类型是 ´e_2´ 和 ´e_3´ 类型在协调后的最小上界。条件表达式 else
符号前的分号会被忽略。
条件表达式的求值方式是先求值 ´e_1´。如果结果为 true
,则返回求值 ´e_2´ 的结果,否则返回求值 ´e_3´ 的结果。
条件表达式的简短形式省略了 else 部分。条件表达式 if (´e_1´) ´e_2´
的求值方式与 if (´e_1´) ´e_2´ else ()
相同。
While 循环表达式
while 循环表达式 while (´e_1´) ´e_2´
的类型和求值方式与 whileLoop (´e_1´) (´e_2´)
的应用相同,其中假设方法 whileLoop
定义如下。
For 推导和 For 循环
for 循环 for (´\mathit{enums}\,´) ´e´
对枚举器 ´\mathit{enums}´ 生成的每个绑定执行表达式 ´e´。for 推导 for (´\mathit{enums}\,´) yield ´e´
对枚举器 ´\mathit{enums}´ 生成的每个绑定求值表达式 ´e´ 并收集结果。枚举器序列始终以生成器开头;之后可以是其他生成器、值定义或守卫。
生成器 ´p´ <- ´e´
从表达式 ´e´ 生成绑定,该表达式以某种方式与模式 ´p´ 匹配。可选地,case
可以出现在生成器模式之前,这在 Scala 2 中没有意义,但在Scala 3 中如果 p
不是不可反驳的,则需要。
一个值定义´p´ = ´e´
将值名称 ´p´(或模式 ´p´ 中的多个名称)绑定到表达式 ´e´ 的求值结果。一个守卫if ´e´
包含一个布尔表达式,用于限制枚举绑定。生成器和守卫的精确含义由转换为对四种方法的调用来定义:map
、withFilter
、flatMap
和 foreach
。这些方法可以针对不同的载体类型以不同的方式实现。
翻译方案如下。第一步,每个生成器 ´p´ <- ´e´
,其中 ´p´ 对于 ´e´ 的类型不是 不可反驳的,都会被替换为
然后,重复应用以下规则,直到所有推导式都被消除。
- 一个 for 推导式
for (´p´ <- ´e\,´) yield ´e'´
被翻译为´e´.map { case ´p´ => ´e'´ }
。 - 一个 for 循环
for (´p´ <- ´e\,´) ´e'´
被翻译为´e´.foreach { case ´p´ => ´e'´ }
。 一个 for 推导式
其中
...
是(可能是空的)生成器、定义或守卫的序列,被翻译为一个 for 循环
其中
...
是(可能是空的)生成器、定义或守卫的序列,被翻译为一个生成器
´p´ <- ´e´
后面跟着一个守卫if ´g´
被翻译为一个单独的生成器´p´ <- ´e´.withFilter((´x_1, ..., x_n´) => ´g\,´)
,其中 ´x_1, ..., x_n´ 是 ´p´ 的自由变量。一个生成器
´p´ <- ´e´
后面跟着一个值定义´p'´ = ´e'´
被翻译为以下值对生成器,其中 ´x´ 和 ´x'´ 是新的名称
示例
以下代码生成所有介于 ´1´ 和 ´n-1´ 之间的数字对,它们的和是素数。
for 推导式被翻译为
示例
for 推导式可以用来简洁地表达向量和矩阵算法。例如,以下是一个计算给定矩阵转置的方法
以下是一个计算两个向量点积的方法
最后,以下是一个计算两个矩阵乘积的方法。与 命令式版本 进行比较。
上面的代码利用了 map
、flatMap
、withFilter
和 foreach
为 scala.Array
类实例定义的事实。
返回表达式
一个返回表达式 return ´e´
必须出现在某个封闭的用户定义方法的主体内部。源程序中最内层的封闭方法 ´m´ 必须具有显式声明的结果类型,并且 ´e´ 的类型必须符合该类型。
返回表达式计算表达式 ´e´ 并将其值作为 ´m´ 的结果返回。返回表达式之后的任何语句或表达式的计算都会被省略。返回表达式的类型是 scala.Nothing
。
表达式 ´e´ 可以省略。返回表达式 return
被类型检查并计算,就好像它是 return ()
一样。
非局部返回(已弃用)
从嵌套函数内部返回方法已弃用。
它是通过抛出和捕获 scala.runtime.NonLocalReturnControl
来实现的。返回点和封闭方法之间的任何异常捕获可能会看到并捕获该异常。一个关键比较确保该异常只会被终止返回的方法实例捕获。
如果返回表达式本身是匿名函数的一部分,则封闭方法 ´m´ 可能在返回表达式执行之前已经返回。在这种情况下,抛出的 scala.runtime.NonLocalReturnControl
不会被捕获,并将向上传播到调用堆栈。
抛出表达式
抛出表达式 throw ´e´
会计算表达式 ´e´。此表达式的类型必须符合 Throwable
。如果 ´e´ 计算为异常引用,则评估将中止并抛出异常。如果 ´e´ 计算为 null
,则评估将改为中止并抛出 NullPointerException
。如果存在一个活动 try
表达式 处理抛出的异常,则评估将从处理程序恢复;否则,执行 throw
的线程将中止。抛出表达式的类型为 scala.Nothing
。
尝试表达式
尝试表达式 形式为 try ´b´ catch ´h´
,其中处理程序 ´h´ 通常是 模式匹配匿名函数
如果处理程序是单个 ExprCaseClause
,则它是该 ExprCaseClause
的简写,包装在模式匹配匿名函数中。
此表达式通过计算块 ´b´ 来计算。如果计算 ´b´ 不会导致抛出异常,则返回 ´b´ 的结果。否则,处理程序 ´h´ 将应用于抛出的异常。如果处理程序包含与抛出的异常匹配的 case,则调用第一个这样的 case。如果处理程序不包含与抛出的异常匹配的 case,则重新抛出异常。更一般地说,如果处理程序是 PartialFunction
,则仅当它在给定异常处定义时才应用它。
令 ´\mathit{pt}´ 为尝试表达式的预期类型。表达式 ´b´ 预计符合 ´\mathit{pt}´。处理程序 ´h´ 预计符合类型 scala.Function[scala.Throwable, ´\mathit{pt}\,´]
。尝试表达式的类型是 ´b´ 的类型和 ´h´ 的结果类型的 最小上界。
尝试表达式 try ´b´ finally ´e´
会计算表达式 ´b´。如果计算 ´b´ 不会导致抛出异常,则计算表达式 ´e´。如果在计算 ´e´ 期间抛出异常,则尝试表达式的计算将中止并抛出异常。如果在计算 ´e´ 期间没有抛出异常,则 ´b´ 的结果将作为尝试表达式的结果返回。
如果在计算 ´b´ 期间抛出异常,则 finally 块 ´e´ 也将被计算。如果在计算 ´e´ 期间抛出另一个异常 ´e´,则尝试表达式的计算将中止并抛出异常。如果在计算 ´e´ 期间没有抛出异常,则一旦 ´e´ 的计算完成,就会重新抛出在 ´b´ 中抛出的原始异常。表达式 ´b´ 预计符合尝试表达式的预期类型。finally 表达式 ´e´ 预计符合类型 Unit
。
尝试表达式 try ´b´ catch ´e_1´ finally ´e_2´
是 try { try ´b´ catch ´e_1´ } finally ´e_2´
的简写。
匿名函数
匿名函数的元数为 ´n´,(´x_1´: ´T_1, ..., x_n´: ´T_n´) => e
将类型为 ´T_i´ 的参数 ´x_i´ 映射到表达式 ´e´ 给出的结果。每个形式参数 ´x_i´ 的作用域为 ´e´。形式参数必须具有成对不同的名称。类型绑定可以省略,在这种情况下,编译器将尝试推断有效的绑定。
注意:() => ´e´
定义了一个零元函数 (´n´ = 0),而不是例如 (_: Unit) => ´e´
。
对于单个无类型形式参数,(´x\,´) => ´e´
可以简写为 ´x´ => ´e´
。如果一个带有单个类型化参数的匿名函数 (´x´: ´T\,´) => ´e´
作为块的结果表达式出现,它可以简写为 ´x´: ´T´ => e
。
形式参数也可以是使用下划线 _
表示的通配符。在这种情况下,将任意选择一个新的参数名称。
匿名函数的命名参数可以选择性地在前面加上 implicit
修饰符。在这种情况下,参数被标记为 implicit
;但是,参数部分本身不计为 隐式参数部分。因此,匿名函数的参数始终必须显式给出。
翻译
如果匿名函数的预期类型为 scala.Function´n´[´S_1´, ..., ´S_n´, ´R\,´]
形状,或者可以 SAM 转换 为这种函数类型,则参数 ´x_i´
的类型 ´T_i´
可以省略,只要 ´S_i´
在预期类型中定义,并且假设 ´T_i´ = ´S_i´
。此外,对 ´e´ 进行类型检查时的预期类型为 ´R´。
如果没有函数字面量的预期类型,则所有形式参数类型 ´T_i´
必须显式指定,并且 ´e´ 的预期类型未定义。匿名函数的类型为 scala.Function´n´[´T_1´, ..., ´T_n´, ´R\,´]
,其中 ´R´ 是 ´e´ 的 打包类型。´R´ 必须等效于不引用任何形式参数 ´x_i´ 的类型。
匿名函数的最终运行时值由预期类型决定
- 内置函数类型的子类之一,
scala.Function´n´[´S_1, ..., S_n´, ´R\,´]
(其中 ´S_i´ 和 ´R´ 完全定义), - 单抽象方法 (SAM) 类型;
PartialFunction[´T´, ´U´]
- 其他类型。
标准匿名函数的评估方式与以下实例创建表达式相同
对于 SAM 类型,相同的评估也适用,只是实例化的类型由 SAM 类型给出,并且实现的方法是该类型的单个抽象方法成员。
底层平台可能提供更有效的方式来构建这些实例,例如 Java 8 的 invokedynamic
字节码和 LambdaMetaFactory
类。
当需要 PartialFunction
时,会合成一个额外的成员 isDefinedAt
,它只返回 true
。但是,如果函数字面量具有 x => x match { $...$ }
形状,则 isDefinedAt
将从匹配表达式中的模式匹配推导出来:匹配表达式中的每个情况都评估为 true
,如果不存在默认情况,则添加一个评估为 false
的默认情况。有关如何实现它的更多详细信息,请参阅 "模式匹配匿名函数"。
示例
匿名函数示例
匿名函数的占位符语法
表达式(语法类别为 Expr
)可以在标识符合法的位置包含嵌入的下划线符号 _
。这样的表达式表示一个匿名函数,其中后续出现的下划线表示连续的参数。
定义一个下划线部分为形式为 _:´T´
的表达式,其中 ´T´ 是一个类型,或者形式为 _
,前提是下划线不作为类型断言 _:´T´
的表达式部分出现。
如果一个语法类别为 Expr
的表达式 ´e´ 绑定了一个下划线部分 ´u´,则以下两个条件必须成立:(1)´e´ 恰好包含 ´u´,并且(2)没有其他语法类别为 Expr
的表达式恰好包含在 ´e´ 中,并且本身恰好包含 ´u´。
如果一个表达式 ´e´ 绑定了按此顺序排列的下划线部分 ´u_1, ..., u_n´,则它等效于匿名函数 (´u'_1´, ... ´u'_n´) => ´e'´
,其中每个 ´u_i'´ 是通过用一个新的标识符替换 ´u_i´ 中的下划线得到的,而 ´e'´ 是通过用 ´u_i'´ 替换 ´e´ 中的每个下划线部分 ´u_i´ 得到的。
示例
左栏中的匿名函数使用占位符语法。每个函数都等效于其右侧的匿名函数。
_ + 1 |
x => x + 1 |
_ * _ |
(x1, x2) => x1 * x2 |
(_: Int) * 2 |
(x: Int) => (x: Int) * 2 |
if (_) x else y |
z => if (z) x else y |
_.map(f) |
x => x.map(f) |
_.map(_ + 1) |
x => x.map(y => y + 1) |
常量表达式
常量表达式是指 Scala 编译器可以计算为常量的表达式。 “常量表达式”的定义取决于平台,但至少包括以下形式的表达式
- 值类的字面量,例如整数
- 字符串字面量
- 使用
Predef.classOf
构造的类 - 来自底层平台的枚举的元素
- 字面量数组,形式为
Array´(c_1, ..., c_n)´
,其中所有 ´c_i´ 都本身是常量表达式 - 由 常量值定义 定义的标识符。
语句
语句作为块和模板的一部分出现。语句可以是导入、定义或表达式,也可以为空。用作语句的表达式可以具有任意值类型。表达式语句 ´e´ 通过计算 ´e´ 并丢弃计算结果来进行计算。
块语句可能是定义,它们在块中绑定局部名称。所有块局部定义中允许的唯一修饰符是 implicit
。在类或对象定义之前添加前缀时,还允许修饰符 abstract
、final
和 sealed
。
语句序列的计算需要按语句编写的顺序计算语句。
协调
表达式列表的协调尝试调整 Int
字面量以匹配兄弟树的类型。例如,当编写
在没有协调的情况下,推断出的元素类型将是 AnyVal
。协调将整数字面量 6
转换为双精度字面量 6.0
,以便元素类型变为 Double
。
形式上,给定一个类型为 ´T_1, ..., T_n´ 的表达式列表 ´e_1, ..., e_n´,协调的行为如下
- 如果存在预期类型,则返回原始列表。
- 否则,如果存在 ´T_i´ 不是原始数字类型(
Char
、Byte
、Short
、Int
、Long
、Float
、Double
),则返回原始列表。 - 否则,
- 将 ´e_i´ 分成整数字面量 ´f_j´ 和其他表达式 ´g_k´。
- 如果所有 ´g_k´ 具有相同的数字类型 ´T´(可能在扩展后),并且所有整数字面量 ´f_j´ 都可以无损地转换为 ´T´,则返回 ´e_i´ 列表,其中每个 int 字面量都被转换为 ´T´。
- 否则,返回原始列表。
隐式转换
隐式转换可以应用于类型与其预期类型不匹配的表达式、选择中的限定符以及未应用的方法。下一节将介绍可用的隐式转换。
值转换
以下七种隐式转换可以应用于具有某个值类型 ´T´ 且使用某个预期类型 ´\mathit{pt}´ 进行类型检查的表达式 ´e´。
静态重载解析
如果一个表达式表示一个类的多个可能成员,则应用重载解析来选择一个唯一的成员。
类型实例化
多态类型表达式 ´e´
它不作为类型应用的函数部分出现,通过使用局部类型推断确定类型变量 ´a_1, ..., a_n´
的实例类型 ´T_1, ..., T_n´
并隐式地将 ´e´ 嵌入到类型应用 ´e´[´T_1, ..., T_n´]
中,转换为 ´T´ 的类型实例。
数字字面量转换
如果期望类型为 Byte
、Short
、Long
或 Char
,并且表达式 ´e´ 是一个适合该类型范围内的 Int
字面量,则将其转换为该类型中的相同字面量。
同样,如果期望类型为 Float
或 Double
,并且表达式 ´e´ 是一个适合该类型范围内的数字字面量(任何类型),则将其转换为该类型中的相同字面量。
值丢弃
如果 ´e´ 具有某种值类型,并且期望类型为 Unit
,则通过将其嵌入到项 { ´e´; () }
中,将 ´e´ 转换为期望类型。
SAM 转换
如果满足以下条件,则函数类型为 (T1, ..., TN) => T
的表达式 (p1, ..., pN) => body
可以 sam 转换为期望类型 S
S
的类C
声明了一个具有签名(p1: A1, ..., pN: AN): R
的抽象方法m
;- 除了
m
之外,C
不得声明或继承任何其他延迟值成员; - 方法
m
必须具有单个参数列表; - 必须存在一个类型
U
,它是S
的子类型,以便表达式new U { final def m(p1: A1, ..., pN: AN): R = body }
类型正确(符合期望类型S
); - 出于作用域的目的,
m
应被视为静态成员(U
的成员不在body
的作用域内); (A1, ..., AN) => R
是(T1, ..., TN) => T
的子类型(满足此条件驱动S
中未知类型参数的类型推断);
请注意,针对 SAM 的函数字面量不一定编译为上述实例创建表达式。这取决于平台。
因此,
- 如果类
C
定义了一个构造函数,则该构造函数必须是可访问的,并且必须定义一个且仅一个空参数列表; - 类
C
不能是final
或sealed
(为简单起见,我们忽略了在与密封类相同的编译单元中进行 SAM 转换的可能性); m
不能是多态的;- 必须能够通过推断
C
的任何未知类型参数,从S
推导出一个完全定义的类型U
。
最后,我们施加了一些实现限制(这些限制可能会在将来的版本中解除)
C
不能是嵌套的或局部的(它不能捕获其环境,因为这会导致非零参数构造函数)C
的构造函数不能具有隐式参数列表(这简化了类型推断);C
不能声明自类型(这简化了类型推断);C
不能是@specialized
。
查看应用程序
如果前面的转换都不适用,并且 ´e´ 的类型不符合预期类型 ´\mathit{pt}´,则尝试使用 视图 将 ´e´ 转换为预期类型。
对 Dynamic
的选择
如果前面的转换都不适用,并且 ´e´ 是选择 ´e.x´ 的前缀,并且 ´e´ 的类型符合类 scala.Dynamic
,则根据 动态成员选择 的规则重写选择。
方法转换
以下四种隐式转换可以应用于未应用于某些参数列表的方法。
评估
类型为 => ´T´
的无参数方法 ´m´ 始终通过评估 ´m´ 绑定的表达式转换为类型 ´T´。
隐式应用
如果方法只接受隐式参数,则根据 此处 的规则传递隐式参数。
Eta 扩展
否则,如果方法不是构造函数,并且预期类型 ´\mathit{pt}´ 是函数类型,或者对于非零元方法,是 可转换为函数类型 的类型 ´(\mathit{Ts}') \Rightarrow T'´,则对表达式 ´e´ 执行 Eta 扩展。
(零元方法的例外是为了避免由于意外的 SAM 转换而导致的意外情况。)
空应用程序
否则,如果 ´e´ 的方法类型为 ´()T´,则它会隐式地应用于空参数列表,从而产生 ´e()´。
重载解析
如果标识符或选择 ´e´ 引用类的多个成员,则引用上下文用于标识唯一的成员。这取决于 ´e´ 是否用作方法。令 ´\mathscr{A}´ 为 ´e´ 引用的成员集。
首先假设 ´e´ 在应用程序中作为函数出现,如 ´e´(´e_1´, ..., ´e_m´)
中所示。
首先根据参数的形状确定一组可能适用的方法。
参数表达式 ´e´ 的形状,写为 ´\mathit{shape}(e)´,是一个类型,定义如下
- 对于函数表达式
(´p_1´: ´T_1, ..., p_n´: ´T_n´) => ´b´: (Any, ..., Any) => ´\mathit{shape}(b)´
,其中Any
在参数类型中出现 ´n´ 次。 - 对于模式匹配匿名函数定义
{ case ... }
:PartialFunction[Any, Nothing]
。 - 对于命名参数
´n´ = ´e´
:´\mathit{shape}(e)´。 - 对于所有其他表达式:
Nothing
。
令 ´\mathscr{B}´ 为 ´\mathscr{A}´ 中对类型为 ´(\mathit{shape}(e_1), ..., \mathit{shape}(e_n))´ 的表达式 ´(e_1, ..., e_n)´ 适用 的备选方案集。如果 ´\mathscr{B}´ 中恰好有一个备选方案,则选择该备选方案。
否则,令 ´S_1, ..., S_m´ 为通过以下方式对每个参数进行类型化而获得的类型列表。
通常,对参数进行类型化时不使用预期类型,除非所有备选方案都明确为该参数指定相同的参数类型(由于例如元数差异而导致的参数类型缺失被视为 NoType
,因此诉诸于没有预期类型),或者当尝试传播更多类型信息以帮助推断高阶函数参数类型时,如下所述。
高阶函数参数类型推断的直觉是,所有参数必须是函数类型的(PartialFunction
、FunctionN
或一些等效的 SAM 类型),而这些函数类型又必须定义相同的更高阶参数类型集,这样它们才能安全地用作重载方法给定参数的预期类型,而不会不必要地排除任何备选方案。目的不是为了引导重载解析,而是为了保留足够的类型信息,以便将参数(函数字面量或 eta 扩展方法)的类型推断引导到这个重载方法。
请注意,预期类型驱动 eta 扩展(除非预期函数类型,否则不会执行),以及推断函数字面量中省略的参数类型。
更准确地说,参数 ´e_i´
的类型为预期类型,该预期类型是从每个备选方案中找到的 ´i´
个参数类型派生的(对于备选方案 ´j´
和参数位置 ´i´
,将这些类型称为 ´T_{ij}´
),当所有 ´T_{ij}´
都是函数类型 ´(A_{1j},..., A_{nj}) => ?´
(或等效的 PartialFunction
或 SAM)时,它们具有一定的元数 ´n´
,并且它们的参数类型 ´A_{kj}´
在所有重载 ´j´
中对于给定的 ´k´
都是相同的。然后,´e_i´
的预期类型如下派生:
- 如果对于某个重载
´j´
,´T_{ij}´
的类型符号是PartialFunction
,则我们使用´PartialFunction[A_{1j},..., A_{nj}, ?]´
; - 否则,如果对于某个
´j´
,´T_{ij}´
是FunctionN
,则预期类型是´FunctionN[A_{1j},..., A_{nj}, ?]´
; - 否则,如果对于所有
´j´
,´T_{ij}´
都是同一个类的 SAM 类型,定义参数类型´A_{1j},..., A_{nj}´
(以及可能不同的结果类型),则预期类型将编码这些参数类型和 SAM 类。
对于 ´\mathscr{B}´
中的每个成员 ´m´
,确定它是否适用于类型为 ´S_1, ..., S_m´
的表达式 (´e_1, ..., e_m´
)。
如果 ´\mathscr{B}´
中没有成员适用,则为错误。如果只有一个适用的备选方案,则选择该备选方案。否则,令 ´\mathscr{CC}´
为不使用任何默认参数应用于 ´e_1, ..., e_m´
的适用备选方案集。
如果 ´\mathscr{CC}´
为空,则再次为错误。否则,根据以下关于“与...一样具体”和“比...更具体”的定义,从 ´\mathscr{CC}´
中的备选方案中选择最具体的备选方案。
- 类型为
(´p_1:T_1, ..., p_n:T_n´)´U´
的参数化方法 ´m´ 比其他类型为 ´S´ 的成员 ´m'´ 更具体,如果 ´m'´ 可以应用于类型为 ´T_1, ..., T_n´ 的参数(´p_1, ..., p_n´)
。如果最后一个参数´p_n´
的类型为可变参数类型´T*´
,那么m
必须适用于任意数量的´T´
参数(这意味着它也必须是一个可变参数方法)。 - 类型为
[´a_1´ >: ´L_1´ <: ´U_1, ..., a_n´ >: ´L_n´ <: ´U_n´]´T´
的多态方法比其他类型为 ´S´ 的成员 ´m'´ 更具体,如果在假设对于 ´i = 1, ..., n´ 每个 ´a_i´ 都是一个从下界为 ´L_i´,从上界为 ´U_i´ 的抽象类型名称的情况下,´T´ 比 ´S´ 更具体。 - 任何其他类型 ´T´ 的成员
- 总是比参数化方法或多态方法更具体。
- 比任何其他类型 ´S´ 的成员 ´m'´ 更具体,如果 ´T´ 与 ´S´ 兼容。
备选方案 ´A´ 相对于备选方案 ´B´ 的相对权重是一个介于 0 到 2 之间的数字,定义为以下两项的总和:
- 如果 ´A´ 比 ´B´ 更具体,则为 1,否则为 0,以及
- 如果 ´A´ 定义在从定义 ´B´ 的类或对象派生的类或对象中,则为 1,否则为 0。
如果以下情况之一成立,则类或对象 ´C´ 从类或对象 ´D´ 派生
- ´C´ 是 ´D´ 的子类,或者
- ´C´ 是从 ´D´ 派生的类的伴生对象,或者
- ´D´ 是 ´C´ 派生的类的伴生对象。
如果备选方案 ´A´ 相对于备选方案 ´B´ 的相对权重大于备选方案 ´B´ 相对于备选方案 ´A´ 的相对权重,则备选方案 ´A´ 比备选方案 ´B´ 更具体。
如果 ´\mathscr{CC}´ 中没有比 ´\mathscr{CC}´ 中所有其他备选方案更具体的备选方案,则这是一个错误。
假设接下来 ´e´ 在类型应用中作为方法出现,如 ´e´[´\mathit{targs}\,´]
。然后,选择 ´\mathscr{A}´ 中所有与 ´\mathit{targs}´ 中类型参数数量相同的备选方案。如果不存在这样的备选方案,则为错误。如果存在多个这样的备选方案,则将重载解析再次应用于整个表达式 ´e´[´\mathit{targs}\,´]
。
最后假设 ´e´ 既不在应用中也不在类型应用中作为方法出现。如果给出了预期类型,则令 ´\mathscr{B}´ 为 ´\mathscr{A}´ 中与之兼容的备选方案集。否则,令 ´\mathscr{B}´ 与 ´\mathscr{A}´ 相同。在最后一种情况下,我们选择 ´\mathscr{B}´ 中所有备选方案中最具体的备选方案。如果 ´\mathscr{B}´ 中不存在比 ´\mathscr{B}´ 中所有其他备选方案更具体的备选方案,则为错误。
示例
考虑以下定义
然后,应用 f(b, b)
指的是 ´f´ 的第一个定义,而应用 f(a, a)
指的是第二个定义。现在假设我们添加第三个重载定义
然后,应用 f(a, a)
被拒绝,因为它不明确,因为不存在最具体的适用签名。
局部类型推断
局部类型推断推断要传递给多态类型表达式的类型参数。假设 ´e´ 的类型为 [´a_1´ >: ´L_1´ <: ´U_1, ..., a_n´ >: ´L_n´ <: ´U_n´]´T´ 且没有给出显式类型参数。
局部类型推断将此表达式转换为类型应用 ´e´[´T_1, ..., T_n´]
。类型参数 ´T_1, ..., T_n´ 的选择取决于表达式出现的上下文和预期类型 ´\mathit{pt}´。有三种情况。
情况 1:选择
如果表达式作为带有名称“x”的选择的前缀出现,则类型推断将推迟到整个表达式“e.x”。也就是说,如果“e.x”的类型为“S”,则现在将其视为具有类型“[‘a_1’ >: ‘L_1’ <: ‘U_1’, ..., ‘a_n’ >: ‘L_n’ <: ‘U_n’] ‘S’”的类型,并且依次应用局部类型推断来推断“a_1, ..., a_n”的类型参数,使用“e.x”出现的上下文。
情况 2:值
如果表达式“e”作为值出现,而没有应用于值参数,则通过求解约束系统来推断类型参数,该约束系统将表达式的类型“T”与预期类型“\mathit{pt}”相关联。不失一般性,我们可以假设“T”是值类型;如果它是方法类型,我们应用eta 展开将其转换为函数类型。求解意味着找到类型“T_i”对类型参数“a_i”的替换“\sigma”,使得
- 推断的类型“T_i”中没有一个是单例类型,除非它是对应于对象或常量值定义的单例类型,或者相应的边界“U_i”是
scala.Singleton
的子类型。 - 所有类型参数边界都得到尊重,即“\sigma L_i <: \sigma a_i”和“\sigma a_i <: \sigma U_i”对于“i = 1, ..., n”。
- 表达式的类型符合预期类型,即“\sigma T <: \sigma \mathit{pt}”。
如果不存在这样的替换,则为编译时错误。如果存在多个替换,局部类型推断将为每个类型变量“a_i”选择解空间的最小或最大类型“T_i”。如果类型参数“a_i”在表达式的类型“T”中逆变,则将选择最大类型“T_i”。在所有其他情况下,即如果变量在类型“T”中协变、非变或根本不出现,则将选择最小类型“T_i”。我们将这种替换称为给定约束系统对于类型“T”的最佳解。
情况 3:方法
最后一种情况适用于表达式“e”出现在应用“e(d_1, ..., d_m)”中。在这种情况下,“T”是方法类型“ (p_1:R_1, ..., p_m:R_m)T'”。不失一般性,我们可以假设结果类型“T'”是值类型;如果它是方法类型,我们应用eta 展开将其转换为函数类型。首先使用两种替代方案计算参数表达式“d_j”的类型“S_j”。每个参数表达式“d_j”首先使用预期类型“R_j”进行类型化,其中类型参数“a_1, ..., a_n”被视为类型常量。如果失败,则参数“d_j”改为使用预期类型“R_j'”进行类型化,该类型“R_j'”是通过用未定义替换“a_1, ..., a_n”中的每个类型参数从“R_j”得到的。
在第二步中,类型参数通过求解约束系统来推断,该约束系统将方法类型与预期类型 ´\mathit{pt}´ 和参数类型 ´S_1, ..., S_m´ 关联起来。求解约束系统意味着找到一个类型 ´T_i´ 对类型参数 ´a_i´ 的替换 ´\sigma´,使得
- 推断的类型“T_i”中没有一个是单例类型,除非它是对应于对象或常量值定义的单例类型,或者相应的边界“U_i”是
scala.Singleton
的子类型。 - 所有类型参数边界都得到尊重,即“\sigma L_i <: \sigma a_i”和“\sigma a_i <: \sigma U_i”对于“i = 1, ..., n”。
- 方法的结果类型 ´T'´ 符合预期类型,即 ´\sigma T' <: \sigma \mathit{pt}´。
- 每个参数类型 弱符合 对应的形式参数类型,即 ´\sigma S_j <:_w \sigma R_j´ 对于 ´j = 1, ..., m´。
如果不存在这样的替换,则为编译时错误。如果存在多个解决方案,则选择类型 ´T'´ 的最佳解决方案。
预期类型 ´\mathit{pt}´ 的全部或部分可能未定义。通过添加以下规则,将 符合 规则扩展到这种情况:对于任何类型 ´T´,以下两个语句始终为真:´\mathit{undefined} <: T´ 和 ´T <: \mathit{undefined}´
可能不存在类型变量的最小或最大解,在这种情况下会导致编译时错误。因为 ´<:´ 是一个预序,所以一个解集也可能对一个类型有多个最优解。在这种情况下,Scala 编译器可以自由选择其中任何一个。
示例
考虑以下两个方法
以及定义
cons
的应用使用未定义的预期类型进行类型化。此应用通过局部类型推断完成为 cons[Int](1, nil)
。这里,使用以下推理来推断类型参数 Int
用于类型参数 a
首先,对参数表达式进行类型化。第一个参数 1
的类型为 Int
,而第二个参数 nil
本身是多态的。尝试使用预期类型 List[a]
对 nil
进行类型检查。这会导致约束系统
其中我们用问号标记了 b?
以指示它是一个约束系统中的变量。因为类 List
是协变的,所以此约束的最优解是
在第二步中,为 cons
的类型参数 a
求解以下约束系统
此约束系统的最优解是
所以Int
是a
推断出的类型。
示例
现在考虑以下定义
其中xs
的类型定义为List[Int]
,与之前一样。在这种情况下,局部类型推断过程如下。
首先,对参数表达式进行类型化。第一个参数"abc"
的类型为String
。第二个参数xs
首先尝试使用预期类型List[a]
进行类型化。这将失败,因为List[Int]
不是List[a]
的子类型。因此,尝试第二种策略;现在使用预期类型List[´\mathit{undefined}´]
对xs
进行类型化。这将成功并产生参数类型List[Int]
。
在第二步中,为 cons
的类型参数 a
求解以下约束系统
此约束系统的最优解是
所以scala.Any
是a
推断出的类型。
Eta 扩展
Eta 扩展将方法类型表达式转换为等效的函数类型表达式。它分两步进行。
首先,确定´e´的最大子表达式;假设这些是´e_1, ..., e_m´。对于其中的每一个,创建一个新的名称´x_i´。令´e'´为将´e´中的每个最大子表达式´e_i´替换为相应的新的名称´x_i´后得到的表达式。其次,为方法的每个参数类型´T_i´(´i = 1 , ..., n´)创建一个新的名称´y_i´。然后,eta 转换的结果为
eta 扩展保留了按名参数的行为:相应的实际参数表达式(无参数方法类型的子表达式)不会在扩展的块中进行求值。
动态成员选择
标准 Scala 库定义了一个标记特征scala.Dynamic
。此特征的子类能够通过定义名为applyDynamic
、applyDynamicNamed
、selectDynamic
和updateDynamic
的方法来拦截其实例上的选择和应用。
执行以下重写,假设´e´的类型符合scala.Dynamic
,并且原始表达式在正常规则下无法进行类型检查,如隐式转换的相关小节中完全指定
-
e.m[Ti](xi)
变为e.applyDynamic[Ti]("m")(xi)
-
e.m[Ti]
变为e.selectDynamic[Ti]("m")
-
e.m = x
变为e.updateDynamic("m")(x)
如果应用中存在任何命名参数(其中一个xi
的形状为arg = x
),则其名称将作为传递给applyDynamicNamed
的配对的第一个组件保留(对于缺少的名称,使用""
)
-
e.m[Ti](argi = xi)
变为e.applyDynamicNamed[Ti]("m")(("argi", xi))
最后
-
e.m(x) = y
变为e.selectDynamic("m").update(x, y)
这些方法实际上都没有在 scala.Dynamic
中定义,因此用户可以自由地定义它们,无论是否带有类型参数或隐式参数。