导出子句
导出子句为对象的选定成员定义别名。示例
class BitMap
class InkJet
class Printer:
type PrinterType
def print(bits: BitMap): Unit = ???
def status: List[String] = ???
class Scanner:
def scan(): BitMap = ???
def status: List[String] = ???
class Copier:
private val printUnit = new Printer { type PrinterType = InkJet }
private val scanUnit = new Scanner
export scanUnit.scan
export printUnit.{status as _, *}
def status: List[String] = printUnit.status ++ scanUnit.status
两个 export
子句在类 Copier
中定义以下导出别名
final def scan(): BitMap = scanUnit.scan()
final def print(bits: BitMap): Unit = printUnit.print(bits)
final type PrinterType = printUnit.PrinterType
可以在 Copier
内部以及外部访问它们
val copier = new Copier
copier.print(copier.scan())
export
子句的格式与导入子句相同。其一般形式为
export path . { sel_1, ..., sel_n }
它由限定符表达式 path
组成,该表达式必须是稳定标识符,后跟一个或多个标识别名的选择器 sel_i
。选择器可以采用以下形式之一
- 简单选择器
x
为path
中所有符合条件且名为x
的成员创建别名。 - 重命名选择器
x as y
为path
中所有符合条件且名为x
的成员创建别名,但别名名为y
,而不是x
。 - 省略选择器
x as _
阻止x
被后续通配符选择器指定别名。 - 指定选择器
given x
具有可选类型绑定x
。它为所有符合条件的指定实例创建别名,这些实例符合x
或Any
(如果省略x
),但先前简单、重命名或省略选择器命名的成员除外。 - 通配符选择器
*
为path
中所有符合条件的成员创建别名,但指定实例、编译器生成的合成成员和先前简单、重命名或省略选择器命名的成员除外。
注释- 符合条件的构造函数代理也包括在内,即使它们是合成成员。
- 导出创建的成员也包括在内。它们由编译器创建,但不被视为合成成员。
如果满足以下所有条件,则成员符合条件
- 它的所有者不是包含导出子句的类的基类(*),
- 成员不会覆盖具有所有者(包含导出子句的类的基类)的具体定义。
- 它在导出子句处可访问,
- 它不是构造函数,也不是对象的(合成)类部分,
- 它是一个指定实例(使用
given
声明),当且仅当导出来自指定选择器时。
如果简单或重命名选择器未识别任何符合条件的成员,则为编译时错误。
类型成员由类型定义指定别名,术语成员由方法定义指定别名。例如
object O:
class C(val x: Int)
def m(c: C): Int = c.x + 1
export O.*
// generates
// type C = O.C
// def m(c: O.C): Int = O.m(c)
导出别名复制它们引用的成员的类型和值参数。导出别名始终为 final
。指定实例的别名再次定义为指定(旧式隐式的别名是 implicit
)。扩展的别名再次定义为扩展。内联方法或值的别名再次定义为 inline
。没有其他修饰符可以赋予别名。这对重写有以下影响
- 导出别名不能被重写,因为它们是最终的。
- 导出别名不能重写基类中的具体成员,因为它们未标记为
override
。 - 但是,导出别名可以实现基类的延迟成员。
对于在限定符路径中不引用私有值的公共值定义的导出别名,编译器将其标记为“稳定”,并且其结果类型是别名定义的单例类型。这意味着它们可以用作稳定标识符路径的一部分,即使它们在技术上是方法。例如,以下内容是正确的
class C { type T }
object O { val c: C = ... }
export O.c
def f: c.T = ...
限制
-
导出子句可以出现在类中,也可以出现在顶层。导出子句不能作为块中的语句出现。
-
如果导出子句包含通配符或给定选择器,则禁止其限定符路径引用包。这是因为目前还不知道如何安全地跟踪通配符依赖项到包,以便进行增量编译。
-
导出重命名隐藏了与目标名称匹配的未重命名的导出。例如,以下子句将无效,因为
B
被重命名A as B
隐藏。export {A as B, B} // error: B is hidden
-
导出子句中的重命名必须具有成对不同的目标名称。例如,以下子句将无效
export {A as C, B as C} // error: duplicate renaming
-
像这样的简单重命名导出
export status as stat
尚未得到支持。它们将违反限制,即导出的
a
不能已经是包含导出的对象的成员。此限制可能会在未来解除。
(*) 注意:除非另有说明,本讨论中的“类”一词还包括对象和特征定义。
动机
一个标准建议是优先使用组合而不是继承。这实际上是最低权力原则的应用:组合将组件视为黑匣子,而继承可以通过重写影响组件的内部工作。有时,继承所暗示的紧密耦合是解决问题的最佳方案,但如果这是不必要的,那么组合的松散耦合会更好。
到目前为止,包括 Scala 在内的面向对象语言使使用继承比组合更容易。继承只需要一个extends
子句,而组合则需要详细阐述一系列转发器。因此,从这个意义上说,面向对象语言正在推动程序员采用一种通常过于强大的解决方案。导出子句纠正了这种平衡。它们使组合关系与继承关系一样简洁且易于表达。导出子句还比扩展子句提供了更大的灵活性,因为可以重命名或省略成员。
导出子句还填补了从包对象到顶级定义的转变中出现的空白。这种转变中丢失的一种偶尔有用的习惯用法是包对象从某个类继承。这种习惯用法通常在类似外观模式中使用,以使内部组合的成员对包的用户可用。顶级定义不会包装在用户定义的对象中,因此它们不能继承任何内容。但是,顶级定义可以是导出子句,这以更安全、更灵活的方式支持外观设计模式。
扩展中的导出子句
导出子句也可能出现在扩展中。
示例
class StringOps(x: String):
def *(n: Int): String = ...
def capitalize: String = ...
extension (x: String)
def take(n: Int): String = x.substring(0, n)
def drop(n: Int): String = x.substring(n)
private def moreOps = new StringOps(x)
export moreOps.*
在这种情况下,限定符表达式必须是标识符,它引用同一扩展子句中唯一的无参数扩展方法。导出将为限定符路径结果中的所有可访问的术语成员创建扩展方法。例如,上面的扩展将扩展为
extension (x: String)
def take(n: Int): String = x.substring(0, n)
def drop(n: Int): String = x.substring(n)
private def moreOps = StringOps(x)
def *(n: Int): String = moreOps.*(n)
def capitalize: String = moreOps.capitalize
语法更改
TemplateStat ::= ...
| Export
TopStat ::= ...
| Export
ExtMethod ::= ...
| Export
Export ::= ‘export’ ImportExpr {‘,’ ImportExpr}
ImportExpr ::= SimpleRef {‘.’ id} ‘.’ ImportSpec
ImportSpec ::= NamedSelector
| WildcardSelector
| ‘{’ ImportSelectors) ‘}’
NamedSelector ::= id [‘as’ (id | ‘_’)]
WildCardSelector ::= ‘*’ | ‘given’ [InfixType]
ImportSelectors ::= NamedSelector [‘,’ ImportSelectors]
| WildCardSelector {‘,’ WildCardSelector}
导出子句的详细说明
导出子句引发了有关类型检查期间详细说明顺序的问题。考虑以下示例
class B { val c: Int }
object A { val b = new B }
export A.*
export b.*
export b.*
子句是否合法?如果是,它导出什么?它是否等效于 export A.b.*
?如果我们交换最后两个子句会怎样?
export b.*
export A.*
为了避免诸如此类棘手的问题,我们按如下方式修复导出的详细说明顺序。
当封闭对象或类的类型信息完成时,将处理导出子句。到目前为止,完成包括以下步骤
-
详细说明类的任何注释。
-
详细说明类的参数。
-
详细说明类的 self 类型(如果给定)。
-
将类的所有定义作为类成员输入,并按需完成类型。
-
确定类的所有父类的类型。
使用导出子句,将添加以下步骤
-
计算导出子句中所有路径的类型。
-
输入导出子句中所有路径的合格成员的导出别名。
按顺序执行步骤 6 和 7 非常重要:我们首先计算导出子句中所有路径的类型,然后才将任何导出别名作为类成员输入。这意味着导出子句的路径不能引用同一类的另一个导出子句提供的别名。