基本定义
一个定义引入表示项的名称并为其分配类型,或者引入表示类型的名称并为其分配类型定义。它可以构成对象或类定义的一部分,也可以是块的局部定义。
由定义引入的名称的作用域是包含该定义的整个语句序列。但是,块中的前向引用存在限制:在构成块的语句序列 ´s_1 ... s_n´ 中,如果 ´s_i´ 中的简单名称引用了由 ´s_j´ 定义的实体,其中 ´j \geq i´,那么对于所有 ´s_k´(包括 ´s_i´ 和 ´s_j´ 之间的 ´s_k´),
- ´s_k´ 不能是变量定义。
- 如果 ´s_k´ 是值定义,它必须是延迟的。
此外,在块中,所有项定义必须是具体的,并且不允许不透明类型别名定义。
值定义
抽象值定义 val ´x´: ´T´
引入 ´x´ 作为声明类型为 ´T´ 的值的名称。´T´ 必须显式指定,并且必须是适当类型。
具体值定义 val ´x´: ´T´ = ´e´
定义 ´x´ 作为对 ´e´ 求值结果的值的名称。如果值定义不是递归的,则可以省略声明类型 ´T´,在这种情况下,将假设表达式 ´e´ 的打包类型。如果给出了类型 ´T´,则它必须是适当类型,并且期望 ´e´符合它。
除非定义包含 `lazy` 修饰符,否则对值定义的评估意味着对其右侧 `e` 的评估。值定义的效果是将 `x` 绑定到 `e` 的值,该值被转换为类型 `T`。`lazy` 值定义在第一次访问该值时评估其右侧 `e`。
常量值定义 形式如下
其中 `e` 是一个 常量表达式。必须存在 `final` 修饰符,并且不能给出类型注释。对常量值 `x` 的引用本身被视为常量表达式;在生成的代码中,它们被替换为定义的右侧 `e`。
具体值定义可以选择将 模式 作为左侧。如果 `p` 是除简单名称或名称后跟冒号和类型之外的任何模式,则值定义 `val `p` = `e` 被扩展如下
- 如果模式 `p` 绑定了变量 `x_1, ..., x_n`,其中 `n > 1`
这里,`$x` 是一个新的名称。
- 如果 `p` 具有唯一的绑定变量 `x`
- 如果 `p` 没有绑定变量
示例
以下是值定义的示例
最后两个定义具有以下扩展。
任何定义的值的名称不能以 `_=` 结尾。
值定义 `val `x_1, ..., x_n` : `T` 是值定义序列 `val `x_1` : `T` ; ... ; val `x_n` : `T` 的简写。值定义 `val `p_1, ..., p_n` = `e` 是值定义序列 `val `p_1` = `e` ; ... ; val `p_n` = `e` 的简写。值定义 `val `p_1, ..., p_n: T` = `e` 是值定义序列 `val `p_1: T` = `e` ; ... ; val `p_n: T` = `e` 的简写。
变量定义
抽象变量定义 `var `x` : `T` 等效于getter 方法 `x` 和setter 方法 `x`_= 的定义
类的实现可以使用具体变量定义或通过定义相应的 setter 和 getter 方法来实现定义的抽象变量。
具体变量定义 `var `x` : `T` = `e` 引入一个可变变量,其类型为 `T`,初始值为表达式 `e` 给出的值。类型 `T` 可以省略,在这种情况下,将假定 `e` 的类型。如果给出了 `T`,则它必须是 适当类型,并且预期 `e` 符合它。
变量定义也可以使用 模式 作为左侧。变量定义 var ´p´ = ´e´
,其中 ´p´ 是除简单名称或名称后跟冒号和类型之外的模式,其扩展方式与 值定义 val ´p´ = ´e´
相同,只是 ´p´ 中的自由名称被引入为可变变量,而不是值。
任何已定义变量的名称不能以 _=
结尾。
模板成员的可变变量定义的右侧可以是特殊引用 scala.compiletime.uninitialized
:var ´x´: ´T´ = scala.compiletime.uninitialized
。它引入了一个类型为 ´T´ 的可变字段和一个默认初始值。默认值取决于类型 ´T´,如下所示
默认值 | 类型 ´T´ |
---|---|
0 |
Int 或其子范围类型 |
0L |
Long |
0.0f |
Float |
0.0d |
Double |
false |
Boolean |
() |
Unit |
null |
所有其他类型 |
scala.compiletime.uninitialized
永远不能出现在其他地方。为了与 Scala 2 兼容,语法 var ´x´: ´T´ = _
被接受为等效于使用 uninitialized
。
当它们作为模板的成员出现时,两种形式的具体变量定义也引入了一个 setter 方法 ´x´_=
,它更改当前分配给变量的值。setter 与抽象变量定义的签名相同。然后就不能直接修改分配给变量的值;变异总是通过相应的 setter 进行。
示例
以下示例展示了如何在 Scala 中模拟属性。它定义了一个名为 TimeOfDayVar
的时间值类,其中包含可更新的整型字段,表示小时、分钟和秒。它的实现包含测试,这些测试只允许将合法值分配给这些字段。另一方面,用户代码像访问普通变量一样访问这些字段。
变量定义 var ´x_1, ..., x_n´: ´T´
是变量定义序列 var ´x_1´: ´T´; ...; var ´x_n´: ´T´
的简写。变量定义 var ´x_1, ..., x_n´ = ´e´
是变量定义序列 var ´x_1´ = ´e´; ...; var ´x_n´ = ´e´
的简写。变量定义 var ´x_1, ..., x_n: T´ = ´e´
是变量定义序列 var ´x_1: T´ = ´e´; ...; var ´x_n: T´ = ´e´
的简写。
类型成员定义
类型成员 可以是抽象类型成员、类型别名或不透明类型别名。
一个可能带参数的抽象类型成员定义type ´t´[´\mathit{tps}\,´] >: ´L´ <: ´H´
声明´t´为一个抽象类型。如果省略,´L´和´H´分别默认为Nothing
和scala.Any
。
一个可能带参数的类型别名定义type ´t´[´\mathit{tps}\,´] = ´T´
定义´t´为一个具体类型成员。
一个可能带参数的不透明类型别名定义opaque type ´t´[´\mathit{tps}\,´] >: ´L´ <: ´H´ = ´T´
定义´t´为一个不透明类型别名,具有公共边界>: ´L´ <: ´H´
和私有别名= ´T´
。
如果存在类型参数子句[´\mathit{tps}\,´]
,则根据下一节中的规则对其进行脱糖。
带参数类型定义的脱糖
带参数类型定义被脱糖成一个无参数类型定义,其边界是带有显式方差注释的类型lambda。
类型参数的范围扩展到边界>: ´L´ <: ´U´
或别名= ´T´
以及类型参数子句´\mathit{tps}´本身。高阶类型参数子句(抽象类型构造器´tc´)具有相同类型的范围,限制在类型参数´tc´的定义中。
为了说明嵌套范围,这些定义都是等效的:type t[m[x] <: Bound[x], Bound[x]]
、type t[m[x] <: Bound[x], Bound[y]]
和type t[m[x] <: Bound[x], Bound[_]]
,因为例如´m´的类型参数的范围仅限于´m´的定义。在所有这些中,´t´是一个抽象类型成员,它抽象了两个类型构造器:´m´代表一个接受一个类型参数的类型构造器,并且必须是Bound
的子类型,´t´的第二个类型构造器参数。t[MutableList, Iterable]
是´t´的有效使用。
抽象类型
带参数的抽象类型
被脱糖成一个无参数的抽象类型,如下所示
- 如果
L
符合Nothing
,则
- 否则
如果´\mathit{tps}´中至少有一个包含显式方差注释,则´\mathit{tps'} = \mathit{tps}´,否则我们推断每个类型参数的方差,就像用户编写的类型lambda[´\mathit{tps}\,´] =>> ´H´
一样。
相同的脱糖适用于类型参数。例如
被视为的简写
类型别名
参数化类型别名
被反糖化为非参数化类型别名
其中 ´\mathit{tps'}´ 的计算方式与之前的情况相同。
不透明类型别名
参数化类型别名
被反糖化为非参数化不透明类型别名,如下所示
- 如果
L
符合Nothing
,则
- 否则
其中 ´\mathit{tps'}´ 的计算方式与之前的情况相同。
非参数化类型成员定义
一个抽象类型成员定义 type ´t´ >: ´L´ <: ´H´
声明 ´t´ 为一个抽象类型,其类型定义具有下界类型 ´L´ 和上界类型 ´H´。
如果类型定义出现在类型的成员定义中,则类型的实现可以使用任何类型 ´T´ 来实现 ´t´,只要 ´L <: T <: H´ 成立。如果 ´L´ 不符合 ´H´,则会发生编译时错误。
一个类型别名定义 type ´t´ = ´T´
定义 ´t´ 为类型 ´T´ 的别名。
一个不透明类型别名定义 opaque type ´t´ >: ´L´ <: ´H´ = ´T´
定义 ´t´ 为一个不透明类型别名,具有公共边界 >: ´L´ <: ´H´
和私有别名 = ´T´
。不透明类型别名只能在模板中声明。它不能是 private
,也不能在子类中被覆盖。为了使定义有效,´T´ 必须满足一些约束
- ´L <: T´ 和 ´T <: H´ 必须为真,
- ´T´ 不能是上下文函数类型,以及
- 如果 ´T´ 是一个类型 lambda,则其结果必须是一个正确的类型(即,它不能是柯里化的类型 lambda)。
从其封闭模板内部查看时,不透明类型别名表现为一个类型别名,其类型定义为 = ´T´
。从其他任何地方查看时,它表现为一个抽象类型成员,其类型定义为 >: ´L´ <: ´H´
。有关控制这种双重视图的精确机制,请参见memberType
。
对于定义和类型参数的范围规则,允许类型名称出现在其自身的边界或其右侧。但是,如果类型别名递归地引用定义的类型本身,则这是一个静态错误。也就是说,类型别名type ´t´[´\mathit{tps}\,´] = ´T´
中的类型´T´不能直接或间接引用名称´t´。如果抽象类型直接或间接地是其自身的上下界,这也是错误的。
示例
以下是合法的类型定义
以下是非法的
如果类型别名type ´t´ = ´S´
引用类类型´S´(或引用作为类类型´S´的eta扩展的类型lambda),则名称´t´也可以用作类型´S´对象的构造函数。
示例
假设我们将Pair
设为参数化类Tuple2
的别名,如下所示
因此,对于任何两个类型´S´和´T´,类型Pair[´S´, ´T\,´]
等效于类型Tuple2[´S´, ´T\,´]
。Pair
也可以用作构造函数,而不是Tuple2
,如
类型参数
类型参数出现在类型定义、类定义和方法定义中。在本节中,我们只考虑具有下界>: ´L´
和上界<: ´U´
的类型参数定义,而对上下文界: ´U´
和视图界<% ´U´
的讨论将推迟到这里。
适当类型参数的最一般形式是´@a_1 ... @a_n´ ´\pm´ ´t´ >: ´L´ <: ´U´
。这里,´L´和´U´是下界和上界,它们限制了参数的可能类型参数。如果´L´不符合´U´,则这是一个编译时错误。´\pm´是方差,即+
或-
的可选前缀。一个或多个注释可以放在类型参数之前。
所有类型参数的名称在其封闭的类型参数子句中必须成对不同。类型参数的范围在每种情况下都包括整个类型参数子句。因此,类型参数可能出现在其自身边界或同一子句中其他类型参数的边界的一部分。但是,类型参数不能直接或间接地被自身限制。
类型构造函数参数向类型参数添加嵌套的类型参数子句。类型构造函数参数的最一般形式是´@a_1 ... @a_n \pm t[\mathit{tps}\,]´ >: ´L´ <: ´U´
。
上述范围限制被推广到嵌套类型参数子句的情况,这些子句声明了高阶类型参数。高阶类型参数(类型参数´t´的类型参数)仅在其直接包围的参数子句(可能包括更深嵌套级别的子句)和´t´的边界中可见。因此,它们的名称必须仅与其他可见参数的名称成对不同。由于高阶类型参数的名称通常无关紧要,因此它们可以用‘_’
表示,而‘_’
在任何地方都不可见。
示例
以下是一些格式良好的类型参数子句
以下类型参数子句是非法的
方差注释
方差注解指示参数化类型实例相对于子类型化的变化方式。‘+’ 方差表示协变依赖,‘-’ 方差表示逆变依赖,而缺少方差指示则表示不变依赖。
方差注解限制了注解类型变量在绑定类型参数的类型或类中出现的形式。在类型定义 type ´T´[´\mathit{tps}\,´] = ´S´
、type ´T´[´\mathit{tps}\,´] >: ´L´ <: ´U´
或 opaque type ´T´[´\mathit{tps}\,´] >: ´L´ <: ´U´ = ´S´
中,标记为 ‘+’ 的类型参数只能出现在协变位置,而标记为 ‘-’ 的类型参数只能出现在逆变位置。类似地,对于类定义 class ´C´[´\mathit{tps}\,´](´\mathit{ps}\,´) extends ´T´ { ´x´: ´S´ => ...}
,标记为 ‘+’ 的类型参数只能出现在自类型 ´S´ 和模板 ´T´ 的协变位置,而标记为 ‘-’ 的类型参数只能出现在逆变位置。
类型或模板中类型参数的方差位置定义如下。设协变的反面是逆变,不变的反面是自身。类型或模板的顶层始终处于协变位置。方差位置在以下结构处发生变化。
- 方法参数的方差位置与封闭参数子句的方差位置相反。
- 类型参数的方差位置与封闭类型参数子句的方差位置相反。
- 类型定义或类型参数的下限的方差位置与类型定义或参数的方差位置相反。
- 可变变量的类型始终处于不变位置。
- 类型别名的右侧始终处于不变位置。
- 类型选择
´p.T´
的前缀 ´p´ 始终处于不变位置。 - 对于类型
´S´[´..., T, ...´]
的类型参数 ´T´- 如果 ´S´ 的对应类型参数是不变的,则 ´T´ 处于不变位置。
- 如果 ´S´ 的对应类型参数是逆变的,则 ´T´ 的方差位置与封闭类型
´S´[´..., T, ...´]
的方差位置相反。
对类中 对象私有值、类型、变量或方法 中的类型参数的引用不会检查其方差位置。在这些成员中,类型参数可以出现在任何地方,而不会限制其合法的方差注释。
示例
以下方差注释是合法的。
使用此方差注释,´P´ 的类型实例相对于其参数协变。例如,
如果 ´P´ 的成员是可变变量,则相同的方差注释将变得非法。
如果可变变量是对象私有的,则类定义将再次合法。
示例
以下方差注释是非法的,因为 ´A´ 出现在 append
参数的逆变位置。
可以通过使用下限泛化 append
的类型来避免此问题。
示例
使用该注释,我们有 OutputChannel[AnyRef]
符合 OutputChannel[String]
。也就是说,可以写入任何对象的通道可以替代只能写入字符串的通道。
方法定义
抽象方法定义 具有形式 def ´f\,\mathit{psig}´: ´T´
,其中 ´f´ 是方法的名称,´\mathit{psig}´ 是其参数签名,´T´ 是其结果类型。具体方法定义 def ´f\,\mathit{psig}´: ´T´ = ´e´
还包括一个方法体 ´e´,即定义方法结果的表达式。参数签名由一个可选的类型参数子句 [´\mathit{tps}\,´]
组成,后跟零个或多个值参数子句 (´\mathit{ps}_1´)...(´\mathit{ps}_n´)
。
如果没有类型或项参数子句,方法定义将引入一个具有适当类型的 method,该类型也是其结果类型。否则,它将引入一个具有方法类型的方法,其参数类型和结果类型如给定。
如果给定了方法的声明结果类型,则预期方法体的类型将 符合 方法的声明结果类型。如果方法定义不是递归的,则可以省略结果类型,在这种情况下,它将从方法体的打包类型中确定。
类型参数子句 ´\mathit{tps}´ 由一个或多个 类型定义 组成,这些定义引入类型参数,可能带有边界。类型参数的范围包括整个签名,包括任何类型参数边界以及方法体(如果存在)。
值参数子句 ´\mathit{ps}´ 由零个或多个形式参数绑定组成,例如 ´x´: ´T´
或 ´x: T = e´
,它们绑定值参数并将它们与其类型相关联。
一元运算符即使为空也不得具有显式参数列表。一元运算符是一个名为 "unary_´op´"
的方法,其中 ´op´ 是 +
、-
、!
或 ~
之一。
默认参数
每个值参数可以选择定义一个默认参数。默认参数表达式 ´e´ 的类型检查使用预期类型 ´T'´,该类型通过将方法类型参数在 ´T´ 中的所有出现替换为未定义类型获得。
对于每个具有默认参数的参数 ´p_{i,j}´,都会生成一个名为 ´f\$´default´\$´n
的方法,该方法计算默认参数表达式。这里,´n´ 表示参数在方法定义中的位置。这些方法由类型参数子句 [´\mathit{tps}\,´]
和所有在 ´p_{i,j}´ 之前的值参数子句 (´\mathit{ps}_1´)...(´\mathit{ps}_{i-1}´)
参数化。´f\$´default´\$´n
方法对于用户程序是不可访问的。
示例
在方法
中,默认表达式 0
使用未定义的预期类型进行类型检查。当应用 compare()
时,默认值 0
被插入,T
被实例化为 Int
。计算默认参数的方法具有以下形式
形式值参数名 ´x´ 的作用域包含所有后续参数子句,以及方法返回值类型和方法体(如果存在)。类型参数名和值参数名必须成对不同。
依赖于先前参数的默认值使用实际参数(如果提供),而不是默认参数。
如果 隐式参数 未被隐式搜索找到,则可以使用默认参数提供它。
按名称参数
值参数的类型可以以 =>
为前缀,例如 ´x´: => ´T´
。这种参数的类型是 按名称类型 => ´T´
。这表示相应的参数在方法应用时不会被求值,而是在方法内部的每次使用时被求值。也就是说,参数使用按名称调用进行求值。
对于带有 val
或 var
前缀的类的参数,包括为其隐式生成 val
前缀的 case 类参数,按名称修饰符是不允许的。
示例
定义
表示 whileLoop
的两个参数都使用按名称调用进行求值。
重复参数
参数部分的最后一个值参数可以以 '*'
为后缀,例如 (..., ´x´:´T´*)
。这种重复参数在方法内部的类型是序列类型 scala.Seq[´T´]
。具有重复参数 ´T´*
的方法接受可变数量的 ´T´ 类型参数。也就是说,如果一个类型为 (´p_1:T_1, ..., p_n:T_n, p_s:S´*)´U´
的方法 ´m´ 应用于参数 ´(e_1, ..., e_k)´,其中 ´k \geq n´,则 ´m´ 在该应用中被视为类型 ´(p_1:T_1, ..., p_n:T_n, p_s:S, ..., p_{s'}:S)U´,其中 ´k - n´ 个 ´S´ 类型出现,任何超过 ´p_s´ 的参数名都是新的。此规则的唯一例外是,如果最后一个参数通过 _*
类型注释标记为序列参数。如果上面的 ´m´ 应用于参数 (´e_1, ..., e_n, e'´: _*)
,则 ´m´ 在该应用中的类型被视为 (´p_1:T_1, ... , p_n:T_n,p_{s}:´scala.Seq[´S´])
。
在包含重复参数的参数部分中,不允许定义任何默认参数。
示例
以下方法定义计算可变数量的整数参数的平方和。
以下对该方法的应用依次产生 0
、1
、6
。
此外,假设定义
以下对方法 sum
的应用是非法的
相比之下,以下应用是合法的,并且再次产生结果 6
方法返回值类型推断
覆盖类 C
的基类中其他方法 m'´
的类成员定义 m
可以省略返回值类型,即使它是递归的。在这种情况下,无论 m
是否递归,其返回值类型都将是 m'´
的返回值类型。
示例
假设以下定义
这里,即使方法是递归的,也可以在 C
中省略 factorial
的结果类型。
尾递归调用消除
包含尾部位置自递归调用的方法定义针对堆栈安全进行了优化。作为方法返回之前的最后一个操作的自调用将被替换为跳转到方法开头,就像在 while 循环中一样。兄弟调用(其中一个方法调用自身,但接收者不同)也进行了优化。
此转换在可能的情况下由编译器自动执行。带有注释 scala.annotation.tailrec
的方法定义如果转换不可行,将无法编译。(该注释用于可能导致堆栈溢出的反优化情况。)
导入子句
- 在
NamedSelector
中,=>
只能在ImportSelectors
内部使用,并且等效于as
,将在将来弃用。 - 在
WildcardSelector
中,_
等效于*
,将在将来弃用。
单个 NamedSelector
或 WildcardSelector
的 ImportSpecifier
等效于包含该单个选择器的 ‘{‘ ImportSelectors ‘}‘
列表。
包含多个导入表达式的导入子句 import ´p_1´.´I_1, ..., p_n´.´I_n´
被解释为一系列导入子句 import ´p_1´.´I_1´; ...; import ´p_n´.´I_n´
。
包含单个导入表达式的导入子句的形式为 import ´p´.´I´
,其中 p
是一个 前缀,而 I
是一个导入说明符。导入说明符确定一组可导入的 p
成员的名称,这些名称无需限定即可使用,以及一组可导入的 given
成员,这些成员在隐式范围内可用。p
的成员 m
是可导入的,如果它是 可访问的。导入说明符的最通用形式是导入选择器列表
对于 ´n \geq 0´ 和 ´m \geq 0´,其中通配符 ‘*’
和 ’given’
可能不存在。它们被分解为非给定选择器和给定选择器。
非给定导入
非给定选择器使每个可导入成员 ´p´.´x_i´
在非限定名称 ´y_i´ 下可用。换句话说,每个导入选择器 ´x_i´ as ´y_i´
将 ´p´.´x_i´
重命名为 ´y_i´。当省略 as ´y_i´
时,´y_i´ 被假定为 ´x_i´。如果存在最终通配符 ‘*’
,则所有非 given
可导入成员 ´z´ 的 ´p´ 除了 ´x_1, ..., x_n, y_1, ..., y_n´
之外,也将在其自己的非限定名称下可用。
非给定导入选择器对类型和术语成员的工作方式相同。例如,导入子句 import ´p´.´x´ as ´y´
将术语名称 ´p´.´x´
重命名为术语名称 ´y´,并将类型名称 ´p´.´x´
重命名为类型名称 ´y´。这两个名称中至少有一个必须引用 ´p´ 的可导入成员。
如果导入选择器中的目标是下划线 as _
,则导入选择器会隐藏对源成员的访问,而不是导入它。例如,导入选择器 ´x´ as _
将 ´x´ “重命名”为下划线符号(在用户程序中无法作为名称访问),从而有效地阻止对 ´x´ 的非限定访问。如果在同一个导入选择器列表中存在最终通配符,这将导入之前导入选择器中未提及的所有成员,这将很有用。
由非给定导入子句引入的绑定的范围从导入子句之后立即开始,并扩展到封闭块、模板、包子句或编译单元的结尾,以先到者为准。
给定导入
给定选择器在隐式范围内使所有可导入的 given
和 implicit
成员 ´p´.´x´
可用,使得 ´p.x´
是 ´T_i´ 的子类型。没有类型的裸 given
选择器等效于 given scala.Any
。
给定成员的名称与选择无关,并且不会在非限定名称的正常范围内可用。
示例
考虑对象定义
然后块
等效于块