《Scala编程指南》系列文章将会详细介绍Scala语言。本文为《Scala编程指南》系列的第五章,将介绍Scala 基础面向对象编程。
Scala 基础面向对象编程
Scala 和Java, Python, Ruby, Smalltalk 以及其它类似语言一样,是一种面向对象语言。如果你来自Java 的世界,你会发现对Java 对象模型限制的一些显著改进。
我们假设你先前有过面向对象编程(OOP)的经验,所以我们不会讨论那些最基本的原理,尽管有一些公用术语和概念会在词汇表中提及。你可以参见[Meyer1997] 来获取OOP 的详细介绍,或者[Martin2003] 获取OOP 的最新消息以及“敏捷开发”的相关信息,参见[GOF1995] 来学习设计模式,参见[WirfsBrock2003] 来讨论面向对象的设计观念。
类和对象的基础
让我们来回顾一下Scala OOP 的术语。
注意
我们在前面看到Scala 有声明对象的概念,我们会在“类和对象:状态哪里去了?”章节来讨论它们。我们会使用术语实例来称呼一个类的实例,意思是类的对象或者实例,用来避免两者之间的混淆。
类可以用关键字class 来声明。我们会在后面看到也可以加上一些其它的关键字,例如用final 来防止创建继承类,以及用abstract 表示这个类不能被实例化,这通常是因为它包含或者继承了没有具体定义的成员声明。
一个实例可以用this 关键字来引用自己,这一点和Java 及其类似语言一样。
遵循Scala 的约定,我们使用术语方法(method)来指代实例的函数(function)。有一些面向对象语言使用术语成员函数(member function)。方法定义由def 关键字开始。
和Java 一样,但是和Ruby,Python 有所区别,Scala 允许重载方法。两个或以上的方法可以有同样的名字,只要它们的完整签名是唯一的。签名包含了类型名字,参数列表及其类型,以及方法的返回值。
不过,这里有一个由类型消除引起的例外,这是一个JVM 的特性,但是被Scala 在JVM 和.NET 平台上所利用从而最小化兼容问题。假设两个方法其它方面都一样,只是其中一个接受List[String] 参数,而另外一个接受List[Int] 参数,如下所示。
- // code-examples/BasicOOP/type-erasure-wont-compile.scala
- // WON'T COMPILE
- object Foo {
- def bar(list: List[String]) = list.toString
- def bar(list: List[Int]) = list.size.toString
- }
你会在第二个方法处得到一个编译错误,因为这两个方法在类型消除后拥有一样的签名。
警告
Scala 解释器会让你输入这两个方法。它简单地抛弃了第一个版本。然而,如果你尝试用:load 文件命令去加载上面的那个例子,你会得到一样的错误。
我们会在《Scala 类型系统》详细讨论类型消除。
同样是约定,我们使用术语字段(field)来指代实例的变量。其它语言则通常使用术语属性(attribute),例如Ruby。注意,一个实例的状态就是该实例的字段所呈现的值的联合。
正如我们在《Scala编程指南 更少的字更多的事》中的“变量声明”章节中所讨论的,只读的(“值”)字段用val 关键字来声明,可读写字段则用var 关键字来声明。
Scala 也允许在类中声明类型,正如我们在《Scala编程指南 更少的字更多的事》中的“抽象类型和参数化类型”章节中所见。
我们一般使用术语成员(member)来指代字段,方法或者类型。注意,字段和方法成员(除开类型成员)共享一样的名称空间,这一点和Java 不一样。我们会在《Scala 高级面向对象编程》的“当方法和字段存取器无法区分时:唯一存取的原则”章节来更多的讨论这一点。
最后,引用类型的实例可以用new 关键字创建,和Java,C# 一样。注意,你在使用默认构造函数时可以不用写括号(例如,没有参数的构造函数)。你某些情况下,字面值可以被用来替代new。例如val name = "Programming Scala" 等效于val name = new String("Programming Scala")。
值类型的实例(例如Int,Double 等),和Java 这样的语言中的元类型相对应,永远都用字面值来创建。例如1,3.14 等。实际上,这些类型没有公有构造函数,所以像val i = new Int(1) 这样的表达式是不能编译的。
我们会在“Scala 类型结构”章节讨论引用类型和值类型的区别。
父类
Scala 支持单继承,不支持多继承。一个子(或继承的)类只可以有一个父类(基类)。唯一的例外是Scala 类层级结构中的根,Any,没有父类。
我们已经见过几个父类和子类的例子了。这里是我们在《Scala编程指南 更少的字更多的事》中的“抽象类型和参数化类型”章节里看到的第一个例子的片段。
- // code-examples/TypeLessDoMore/abstract-types-script.scala
- import java.io._
- abstract class BulkReader {
- // ...
- }
- class StringBulkReader(val source: String) extends BulkReader {
- // ...
- }
- class FileBulkReader(val source: File) extends BulkReader {
- // ...
- }
和在Java 一样,关键字extends 指明了父类,在这里就是BulkReader。在Scala 中,extends 也会在一个类把一个trait 作为父亲继承的时候使用(即使当它用with 关键字混入其它traits 的时候也是一样)。而且,extends 也在一个trait 是另外一个trait 或类的继承者的时候使用。是的,traits 可以继承自类。
如果你不继承任何父类,默认的父亲是AnyRef,Any 的一个直接子类。(我们会在“Scala 类型层级结构”章节中讨论Any 和AnyRef 的区别。)
Scala 构造函数
Scala 可以区分主构造函数和0个或多个辅助构造函数。在Scala 里,类的整个主体就是主构造函数。构造函数所需要的任何参数被列于类名之后。我们已经看到过很多例子了,比如我们在《第4章 - Traits》中使用的ButtonWithCallbacks 例子。
- // code-examples/Traits/ui/button-callbacks.scala
- package ui
- class ButtonWithCallbacks(val label: String,
- val clickedCallbacks: List[() => Unit]) extends Widget {
- require(clickedCallbacks != null, "Callback list can't be null!")
- def this(label: String, clickedCallback: () => Unit) =
- this(label, List(clickedCallback))
- def this(label: String) = {
- this(label, Nil)
- println("Warning: button has no click callbacks!")
- }
- def click() = {
- // ... logic to give the appearance of clicking a physical button ...
- clickedCallbacks.foreach(f => f())
- }
- }
类ButtonWithCallbacks 表示了图形用户界面上的一个按钮。它有一个标签和一个回调函数的列表,这些函数会在按钮被点击的时候被调用。每一个回调函数都不接受参数,并且返回Unit。方法click 会遍历回调函数的列表,然后一个个地调用它们。
ButtonWithCallbacks 定义了3个构造函数。主构造函数,类的主题,有一个参数列表来接受标签字符串和回调函数的列表。因为每一个参数都被声明为val, 编译器为每一个参数都生成一个私有字段(会使用一个不同的内部名称),以及名字和参数一致的公有读取方法。“私有”和“公有”在这里的意思和在大多数面向对象语言里一样。我们会在下面的“可见性规则”章节讨论不同的可见性规则和控制它们的关键字。
如果参数有一个var 关键字,一个公有的写方法会被自动生成,并且名字为参数名加下划线等号(_=)。例如,如果label 被声明为var, 对应的写方法则为label_=,而且它会接受一个字符串作为参数。
有时候你可能不希望自动生成这些访问器方法。换句话说,你希望字段是私有的。在val 或者var 之前加上private 关键字,访问器方法就不会被生成。(参见“可见性规则”章节获取更多细节信息。)
注意
对于Java 程序员,Scala 没有遵循s [JavaBeanSpec] 约定 - 字段读取、写方法分别对应get 和set 的前缀,紧接着是第一个字母大写的字段名。我们会在“当方法和字段存取器无法区分时:唯一存取的原则”章节中讨论唯一存取原则时看到原因。不过,你可以在需要时通过scala.reflect.BeanProperty 来获得JavaBeans 风格的访问器,我们会在《第14章 - Scala 工具,库和IDE 支持》中的“JavaBean 属性”章节来讨论这个问题。
当类的一个实例被创建时,每一个参数对应的字段都会被参数自动初始化。初始化这些字段不需要逻辑上的构造函数,这和很多面向对象语言不同。
ButtonWithCallbacks 类主体(换言之,构造函数)的第一个指令是一个保证被传入构造函数的参数列表是一个非空列表的测试。(不过它确实允许一个空的Nil 列表。)它使用了方便的require 函数,这个函数是被自动导入到当前的作用域中的(正如我们将在《第7章 - Scala 对象系统》的“预定义对象”章节所要讨论的)。如果这个列表是null, require 会抛出一个异常。require 函数和它对应的假设对于设计契约式程序非常有用,我们会在《第13章 - 应用程序设计》的“用契约式设计方式构造更佳的设计”章节中讨论这个问题。
这里是ButtonWithCallbacks 的完整Specification(规格)的一部分,它展示了require 指令的作用。
- // code-examples/Traits/ui/button-callbacks-spec.scala
- package ui
- import org.specs._
- object ButtonWithCallbacksSpec extends Specification {
- "A ButtonWithCallbacks" should {
- // ...
- "not be constructable with a null callback list" in {
- val nullList:List[() => Unit] = null
- val errorMessage =
- "requirement failed: Callback list can't be null!"
- (new ButtonWithCallbacks("button1", nullList)) must throwA(
- new IllegalArgumentException(errorMessage))
- }
- }
- }
Scala 甚至使得把null 作为第二个参数传给构造函数变得很困难;它不会再编译时做类型检查。然而,你向上面那样可以把null 赋给一个value。如果我们没有must throwA(...) 子句,我们会看到下面的异常被抛出。
- java.lang.IllegalArgumentException: requirement failed: Callback list can't be null!
- at scala.Predef$.require(Predef.scala:112)
- at ui.ButtonWithCallbacks.(button-callbacks.scala:7)
- ....
ButtonWithCallbacks 定义了两个方便用户使用的辅助构造函数。第一个辅助构造函数接受一个标签和一个单独的回调函数。它调用主构造函数,并且传递给它标签和包含了回调函数的新列表。
第二个辅助构造函数只接受一个标签。它调用主构造函数,并且传入Nil(Nil 表示了一个空的List 对象)。然后构造函数打印出一条警告消息指明没有回调函数,因为列表是不可变的,所以我们没有机会用一个新的值来替代现有的回调函数列表。
为了避免无限递归,Scala 要求每一个辅助构造函数调用在它之前定义的构造函数[ScalaSpec2009]。被调用的构造函数可以是另外一个辅助构造函数或者主构造函数,而且它必须出现在辅助构造函数主体的第一句。额外的过程可以在这个调用之后出现,比如我们例子中的打印出一个警告消息。
注意
因为所有的辅助构造函数最终都会调用主构造函数,它主体中进行的逻辑检查和其它初始化工作会在所有实例被创建的时候执行。
Scala 对构造函数的约束有一些好处。
消除重复
因为辅助构造函数会调用主构造函数,潜在的重复构造逻辑就被大大地消除了。
代码体积的减少
正如例子中所示,当一个或更多的主构造函数参数被声明为val 或者var,Scala 会自动产生一个字段,合适的存取方法(除非它们被定义为private,私有的),以及实例被创建时的初始化逻辑。
不过,这样也有至少一个缺点。
缺少弹性
有时候,迫使所有构造函数都是用同一个构造函数体并不方便。然而,我们发现这样的情况只是极少数。在这种情况下,可能是因为这个类负责了太多东西,而且应该被重构为更小的类。
调用父类构造函数
子类的主构造函数必须调用父类的一个构造函数,无论是主构造函数或者是辅助构造函数。在下面的例子里,类RadioButtonWithCallbacks 会继承ButtonWithCallbacks,并且调用ButtonWithCallbacks 的主构造函数。“Radio”按钮可以被设置为开或者关。
- // code-examples/BasicOOP/ui/radio-button-callbacks.scala
- package ui
- /**
- * Button with two states, on or off, like an old-style,
- * channel-selection button on a radio.
- */
- class RadioButtonWithCallbacks(
- var on: Boolean, label: String, clickedCallbacks: List[() => Unit])
- extends ButtonWithCallbacks(label, clickedCallbacks) {
- def this(on: Boolean, label: String, clickedCallback: () => Unit) =
- this(on, label, List(clickedCallback))
- def this(on: Boolean, label: String) = this(on, label, Nil)
- }
RadioButtonWithCallbacks 的主构造函数接受3个参数,一个开关状态(真或假),一个标签,以及一个回调函数例表。它把标签和回调函数列表传给父类ButtonWithCallbacks。开关状态参数(on)被声明为var,所以是可变的。on 也是每一个单选按钮的私有属性。 为了和父类保持统一,RadioButtonWithCallbacks 还定义了两个辅助构造函数。注意它们必须调用一个之前定义的构造函数,和之前一样。它们不能直接调用ButtonWithCallbacks 的构造函数。为所有类声明这些构造函数可能是乏味的,但是我们在《第4章 - Traits》中探索的技巧可以帮助我们减少这样的重复。
注意
虽然和Java 一样,super 关键字通常被用来调用重写的方法,但是它不能被用作调用父类的构造函数。
嵌套类
Scala 和许多面向对象语言一样,允许你嵌套声明类。假设我们希望所有的部件都有一系列的属性。这些属性可以是大小,颜色,是否可见等。我们可以使用一个简单的map 来保存这些属性,但是我们假设还希望能够控制对这些属性的存取,并且当它们改变时能进行一些其它的操作。
下面的例子展示了我们如何利用从《第4章 - Traits》中的“混合Traits”章节学到的特性来扩展我们原来的Widget 例子。
- // code-examples/BasicOOP/ui/widget.scala
- package ui
- abstract class Widget {
- class Properties {
- import scala.collection.immutable.HashMap
- private var values: Map[String, Any] = new HashMap
- def size = values.size
- def get(key: String) = values.get(key)
- def update(key: String, value: Any) = {
- // Do some preprocessing, e.g., filtering.
- valuesvalues = values.update(key, value)
- // Do some postprocessing.
- }
- }
- val properties = new Properties
- }
我们添加了一个Properties 类,包含了一个私有的,可变的HashMap (HashMap 本身不可变)引用。我们同时加入了3个公有方法来获取大小(例如,定义的属性个数),获取map 中的元素,以及更新map 中对应的元素等。我们可能需要在update 方法上做更多的工作,已经用注释标明。
注意
你可以从上面的例子中看到,Scala 允许在一个类中定义另外一个,或者成为“嵌套”。当你有足够多的功能需要归并到一个类里,并且这个类在仅会被外层类所使用时,一个嵌套类就非常有用。
到这里为止,我们学习了如何声明一个类,如何初始化它们,以及继承的一些基础。在下一个章节,我们会讨论类和对象内部的可见性规则。#p#
可见性规则
注意
为了方便,我们会使用通用的“类型” 这一词语来指代类和Trait,对应的还有成员类型。除非特别声明,否则我们在使用通用术语“成员” 时会包含这些定义。
大多数面向对象语言都有控制类型或者类型成员可见性(作用域)声明的结构。这些结构支持面向对象式的封装,即本质上只有类或者Trait 的公共抽象会被暴露出来,内部实现则被隐藏于视界之下。
对于你的类、对象的用户所希望看到和使用的任何地方你都会想用公共可见性。但是记住,公共可见成员的集合构成了类型暴露出的抽象接口,包括类型的名字。
面向对象设计世界的传统智慧是,字段应该为私有(private)或者受保护的(protected)。如果有存取需求,也应该通过方法来完成,而不是使得所有东西都默认可存取。统一访问原则(参见章节“当存取方法和字段无法区分时:统一访问原则”)归根结底是说我们可以通过方法或字段的直接存取给予用户公共(public)字段的访问语意,只要它对于任务来说是合适的即可。
提示
好的面向对象设计的艺术在于定义最小的,清晰的,有凝聚力的公共抽象层。
类型有两种“用户”: 继承类型,以及使用类型实例的代码。继承类型通常比实例用户需要更多地存取父类型的成员。
Scala 的可见性规则和Java 类似,但是倾向于更统一和灵活。例如,在Java 中,如果一个内部类有一个私有成员,则包含它的外部类是能看到的。在Scala 里,则不能看到,但是Scala 提供了另外一种方式来声明它对于包含它的外部类可见。
和Java,C# 一样,修改可见性的关键字,比如private 和protected,在声明的最开始出现。你会在class,trait 关键字前,val 或者var 前,以及方法的def 前发现它们。
注意
你也可以在类的主构造函数前使用一个可见性修饰符。如果有,把它放在类型名称和类型参数后,参数列表之前。像这样:
- class Restricted[+A] private (name: String) {...}
表格 5.1,“可见域” 总结了可见性的范围。
名称 | 关键字 | 描述 |
public | 没有 | public 成员在任何地方都可见,跨越所有边界 |
protected | protected | protected 成员对于定义它的类型,继承类型以及嵌套类型可见,protected 类型仅在同一个包,子包中可见。 |
private | private | private 成员对于定义它的类型和嵌套类型可见,private 类型仅在同一个包可见。 |
scoped protected | protected[scoped] | 可见性被限制在域scoped 中,它可以是包,类型,或者this(对于成员来说就是该实例,对于类型来说就是它存在的包。参见下面的文字获取更多信息。 |
scoped private | private[scoped] | 和scoped protected 一样,除了继承的时候。(下面会讨论) |
让我们来仔细探索一下这些可见性选项。为了简单,我们会使用字段来作为成员的例子。方法,类型声明的行为和字段是一致的。
注意
不幸的是,你不能对包做任何可见性修饰。因此,一个包永远都是public,即使它没有包含任何public 类型。
Public 可见性
任何没有显式可见性关键字的声明都是“public”,意味着它在任何地方都可见。在Scala 里没有public 关键字。这和Java 恰恰相反,Java 的默认行为是只在当前包里默认是public 可见性(也就是包私有的-“package private”)。其它面向对象语言,比如Ruby,也是默认public 可见性。
- // code-examples/BasicOOP/scoping/public.scala
- package scopeA {
- class PublicClass1 {
- val publicField = 1
- class Nested {
- val nestedField = 1
- }
- val nested = new Nested
- }
- class PublicClass2 extends PublicClass1 {
- val field2 = publicField + 1
- val nField2 = new Nested().nestedField
- }
- }
- package scopeB {
- class PublicClass1B extends scopeA.PublicClass1
- class UsingClass(val publicClass: scopeA.PublicClass1) {
- def method = "UsingClass:" +
- " field: " + publicClass.publicField +
- " nested field: " + publicClass.nested.nestedField
- }
- }
你可以用scalac 编译这个文件,应该不会遇到编译错误。
这些包和类的任何成员都是public 的。主意,scopeB.UsingClass 可以访问scopeA.PublicClass1 和它的成员,包括嵌套类的实例以及它的public 字段。
Protected 可见性
Protected 可见性为实现继承的类型提供了一些好处,因为它需要对其父类型有更多的一些存取权限。任何用protected 关键字声明的成员只对定义它的类型,包括其实例和任何继承类型可见。当应用于类型时,protected 限制其可见性于包含它的package 中。
J对比之下,Java 使得protected 成员对于整个包都可见。Scala 则用scoped (区域的)private 和protected 来控制这样的情况。
- // code-examples/BasicOOP/scoping/protected-wont-compile.scala
- // WON'T COMPILE
- package scopeA {
- class ProtectedClass1(protected val protectedField1: Int) {
- protected val protectedField2 = 1
- def equalFields(other: ProtectedClass1) =
- (protectedField1 == other.protectedField1) &&
- (protectedField1 == other.protectedField1) &&
- (nested == other.nested)
- class Nested {
- protected val nestedField = 1
- }
- protected val nested = new Nested
- }
- class ProtectedClass2 extends ProtectedClass1(1) {
- val field1 = protectedField1
- val field2 = protectedField2
- val nField = new Nested().nestedField // ERROR
- }
- class ProtectedClass3 {
- val protectedClass1 = new ProtectedClass1(1)
- val protectedField1 = protectedClass1.protectedField1 // ERROR
- val protectedField2 = protectedClass1.protectedField2 // ERROR
- val protectedNField = protectedClass1.nested.nestedField // ERROR
- }
- protected class ProtectedClass4
- class ProtectedClass5 extends ProtectedClass4
- protected class ProtectedClass6 extends ProtectedClass4
- }
- package scopeB {
- class ProtectedClass4B extends scopeA.ProtectedClass4 // ERROR
- }
当你用scalac 编译这个文件的时候,你会得到下列输出。(为了配合排版,在N: 行号之前的文件名已经被移除。)
- 16: error: value nestedField cannot be accessed in ProtectedClass2.this.Nested
- val nField = new Nested().nestedField
- ^
- 20: error: value protectedField1 cannot be accessed in scopeA.ProtectedClass1
- val protectedField1 = protectedClass1.protectedField1
- ^
- 21: error: value protectedField2 cannot be accessed in scopeA.ProtectedClass1
- val protectedField2 = protectedClass1.protectedField2
- ^
- 22: error: value nested cannot be accessed in scopeA.ProtectedClass1
- val protectedNField = protectedClass1.nested.nestedField
- ^
- 32: error: class ProtectedClass4 cannot be accessed in package scopeA
- class ProtectedClass4B extends scopeA.ProtectedClass4
- ^
- 5 errors found
列表中的//ERROR 注释标识了无法解析的行。
ProtectedClass2 可以存取ProtectedClass1 的protected 成员,因为它们是继承关系。然而,它不能存取protectedClass1.nested 的protected nestedField 字段。而且,ProtectedClass3 不能存取它使用的ProtectedClass1 实例的protected 成员。
最终,因为ProtectedClass4 被声明为protected,它对于scopeB 包来说不可见。
Private 可见性
Private 可见性完全隐藏了实现的细节,即使对于继承类的实现也一样。任何用private 关键字声明的成员只对定义它的类型可见,包括它的实例。当应用于类型时,private 限制其可见性为包含它的package。
- // code-examples/BasicOOP/scoping/private-wont-compile.scala
- // WON'T COMPILE
- package scopeA {
- class PrivateClass1(private val privateField1: Int) {
- private val privateField2 = 1
- def equalFields(other: PrivateClass1) =
- (privateField1 == other.privateField1) &&
- (privateField2 == other.privateField2) &&
- (nested == other.nested)
- class Nested {
- private val nestedField = 1
- }
- private val nested = new Nested
- }
- class PrivateClass2 extends PrivateClass1(1) {
- val field1 = privateField1 // ERROR
- val field2 = privateField2 // ERROR
- val nField = new Nested().nestedField // ERROR
- }
- class PrivateClass3 {
- val privateClass1 = new PrivateClass1(1)
- val privateField1 = privateClass1.privateField1 // ERROR
- val privateField2 = privateClass1.privateField2 // ERROR
- val privateNField = privateClass1.nested.nestedField // ERROR
- }
- private class PrivateClass4
- class PrivateClass5 extends PrivateClass4 // ERROR
- protected class PrivateClass6 extends PrivateClass4 // ERROR
- private class PrivateClass7 extends PrivateClass4
- }
- package scopeB {
- class PrivateClass4B extends scopeA.PrivateClass4 // ERROR
- }
编译这个文件会产生如下输出。
- 14: error: not found: value privateField1
- val field1 = privateField1
- ^
- 15: error: not found: value privateField2
- val field2 = privateField2
- ^
- 16: error: value nestedField cannot be accessed in PrivateClass2.this.Nested
- val nField = new Nested().nestedField
- ^
- 20: error: value privateField1 cannot be accessed in scopeA.PrivateClass1
- val privateField1 = privateClass1.privateField1
- ^
- 21: error: value privateField2 cannot be accessed in scopeA.PrivateClass1
- val privateField2 = privateClass1.privateField2
- ^
- 22: error: value nested cannot be accessed in scopeA.PrivateClass1
- val privateNField = privateClass1.nested.nestedField
- ^
- 27: error: private class PrivateClass4 escapes its defining scope as part of type scopeA.PrivateClass4
- class PrivateClass5 extends PrivateClass4
- ^
- 28: error: private class PrivateClass4 escapes its defining scope as part of type scopeA.PrivateClass4
- protected class PrivateClass6 extends PrivateClass4
- ^
- 33: error: class PrivateClass4 cannot be accessed in package scopeA
- class PrivateClass4B extends scopeA.PrivateClass4
- ^
- 9 errors found
现在,PrivateClass2 不能访问它的父类PrivateClass1 的private 成员。正如错误消息指出的,它们对于子类来说完全不可见。它们也不能存取嵌套类的private 字段。
正如protected 访问的例子一样,PrivateClass3 不能访问它使用的PrivateClass1 实例的private 成员。不过注意,equalFields 方法可以访问其它实例的private 成员。
PrivateClass5 和PrivateClass6 的声明失败了,因为如果允许的话,它们等于允许PrivateClass4 “跳出它的定义域”。然而,PrivateClass7 的声明成功了,因为它同时被定义为了private。令人好奇的是,我们上一个例子中能够正确地定义一个继承自protected 类的public 类。
最后,和protected 类型声明一样,private 类型不能在包含它的package 之外被继承。
局部 Private 和Protected 可见性
Scala 允许你用scoped private 和protected 可见性声明来更精细地调整可见性的范围。注意,在局部声明中使用proviate 或者protected 是可互换的,因为它们除了应用到继承的成员上的可见性之外,其它都是一样的。
提示
虽然在大多数情况下选择任何一个都能活动相同的效果,在代码中使用private 还是比protected 更常见一些。在Scala 的核心库里,这个比利大概是5:1。
让我们从scoped private 和scoped protected 之间的唯一区别入手,来看看当成员有这些局部性声明的时候,它们在继承机制下是如何工作的。
- // code-examples/BasicOOP/scoping/scope-inheritance-wont-compile.scala
- // WON'T COMPILE
- package scopeA {
- class Class1 {
- private[scopeA] val scopeA_privateField = 1
- protected[scopeA] val scopeA_protectedField = 2
- private[Class1] val class1_privateField = 3
- protected[Class1] val class1_protectedField = 4
- private[this] val this_privateField = 5
- protected[this] val this_protectedField = 6
- }
- class Class2 extends Class1 {
- val field1 = scopeA_privateField
- val field2 = scopeA_protectedField
- val field3 = class1_privateField // ERROR
- val field4 = class1_protectedField
- val field5 = this_privateField // ERROR
- val field6 = this_protectedField
- }
- }
- package scopeB {
- class Class2B extends scopeA.Class1 {
- val field1 = scopeA_privateField // ERROR
- val field2 = scopeA_protectedField
- val field3 = class1_privateField // ERROR
- val field4 = class1_protectedField
- val field5 = this_privateField // ERROR
- val field6 = this_protectedField
- }
- }
编译这个文件会产生如下输出。
- 17: error: not found: value class1_privateField
- val field3 = class1_privateField // ERROR
- ^
- 19: error: not found: value this_privateField
- val field5 = this_privateField // ERROR
- ^
- 26: error: not found: value scopeA_privateField
- val field1 = scopeA_privateField // ERROR
- ^
- 28: error: not found: value class1_privateField
- val field3 = class1_privateField // ERROR
- ^
- 30: error: not found: value this_privateField
- val field5 = this_privateField // ERROR
- ^
- 5 errors found
Class2 里的前两个错误说明,在同一个package 内的继承类,不能引用父类或者this 的scoped private 成员,但是它可以引用包含Class1 和Class2 的package (或者类型)的private 成员。
相比之下,对于package 之外的继承类,它无法访问Class1 的任何一个scoped private 成员。
然而,所有的scoped protected 成员对于两个继承类来说都是可以见的。
我们会在后面剩下的例子和讨论中使用scoped private 声明,因为在Scala 库中,scoped private 比scoped protected 更加常见一些,前面的继承情况并不是其中一个因素。
首先,让我们从最严格的可见性开始,private[this],它也对类型成员起作用。
- // code-examples/BasicOOP/scoping/private-this-wont-compile.scala
- // WON'T COMPILE
- package scopeA {
- class PrivateClass1(private[this] val privateField1: Int) {
- private[this] val privateField2 = 1
- def equalFields(other: PrivateClass1) =
- (privateField1 == other.privateField1) && // ERROR
- (privateField2 == other.privateField2) &&
- (nested == other.nested)
- class Nested {
- private[this] val nestedField = 1
- }
- private[this] val nested = new Nested
- }
- class PrivateClass2 extends PrivateClass1(1) {
- val field1 = privateField1 // ERROR
- val field2 = privateField2 // ERROR
- val nField = new Nested().nestedField // ERROR
- }
- class PrivateClass3 {
- val privateClass1 = new PrivateClass1(1)
- val privateField1 = privateClass1.privateField1 // ERROR
- val privateField2 = privateClass1.privateField2 // ERROR
- val privateNField = privateClass1.nested.nestedField // ERROR
- }
- }
编译这个文件会产生如下输出。
- 5: error: value privateField1 is not a member of scopeA.PrivateClass1
- (privateField1 == other.privateField1) &&
- ^
- 14: error: not found: value privateField1
- val field1 = privateField1
- ^
- 15: error: not found: value privateField2
- val field2 = privateField2
- ^
- 16: error: value nestedField is not a member of PrivateClass2.this.Nested
- val nField = new Nested().nestedField
- ^
- 20: error: value privateField1 is not a member of scopeA.PrivateClass1
- val privateField1 = privateClass1.privateField1
- ^
- 21: error: value privateField2 is not a member of scopeA.PrivateClass1
- val privateField2 = privateClass1.privateField2
- ^
- 22: error: value nested is not a member of scopeA.PrivateClass1
- val privateNField = privateClass1.nested.nestedField
- ^
- 7 errors found
注意
第6 到8 行无法解析。因为它们是第5 行开始的表达式的一部分,编译器在遇到第一个错误之后就会停住。
这些private[this] 成员仅对同一个实例内的成员可见。同一个类的不同实例之间无法访问对方的private[this] 成员,所以equalFields 方法无法通过解析。
否则,类成员的可见性就和没有域限定符的private 一样了。
当用private[this] 声明一个类型时,this 的使用被有效的绑定到了包含它的package 里,正如这里所展示的。
- // code-examples/BasicOOP/scoping/private-this-pkg-wont-compile.scala
- // WON'T COMPILE
- package scopeA {
- private[this] class PrivateClass1
- package scopeA2 {
- private[this] class PrivateClass2
- }
- class PrivateClass3 extends PrivateClass1 // ERROR
- protected class PrivateClass4 extends PrivateClass1 // ERROR
- private class PrivateClass5 extends PrivateClass1
- private[this] class PrivateClass6 extends PrivateClass1
- private[this] class PrivateClass7 extends scopeA2.PrivateClass2 // ERROR
- }
- package scopeB {
- class PrivateClass1B extends scopeA.PrivateClass1 // ERROR
- }
编译这个文件会产生如下输出。
- 8: error: private class PrivateClass1 escapes its defining scope as part of type scopeA.PrivateClass1
- class PrivateClass3 extends PrivateClass1
- ^
- 9: error: private class PrivateClass1 escapes its defining scope as part of type scopeA.PrivateClass1
- protected class PrivateClass4 extends PrivateClass1
- ^
- 13: error: type PrivateClass2 is not a member of package scopeA.scopeA2
- private[this] class PrivateClass7 extends scopeA2.PrivateClass2
- ^
- 17: error: type PrivateClass1 is not a member of package scopeA
- class PrivateClass1B extends scopeA.PrivateClass1
- ^
- four errors found
在同一个package 中,尝试声明一个public 或者protected 子类会失败。只有private 和private[this] 子类是允许的。而且,PrivateClass2 在scopeA2 里,所以你不能在scopeA2 之外声明它。简单地尝试在无关的scopeB 中声明一个使用PrivateClass1 的类也失败了。
因此,当应用到类型时,private[this] 和Java 的package private 可见性一致。
下面,让我们来检查类型级别的可见性,private[T],T 是一个类型。
- // code-examples/BasicOOP/scoping/private-type-wont-compile.scala
- // WON'T COMPILE
- package scopeA {
- class PrivateClass1(private[PrivateClass1] val privateField1: Int) {
- private[PrivateClass1] val privateField2 = 1
- def equalFields(other: PrivateClass1) =
- (privateField1 == other.privateField1) &&
- (privateField2 == other.privateField2) &&
- (nested == other.nested)
- class Nested {
- private[Nested] val nestedField = 1
- }
- private[PrivateClass1] val nested = new Nested
- val nestednestedNested = nested.nestedField // ERROR
- }
- class PrivateClass2 extends PrivateClass1(1) {
- val field1 = privateField1 // ERROR
- val field2 = privateField2 // ERROR
- val nField = new Nested().nestedField // ERROR
- }
- class PrivateClass3 {
- val privateClass1 = new PrivateClass1(1)
- val privateField1 = privateClass1.privateField1 // ERROR
- val privateField2 = privateClass1.privateField2 // ERROR
- val privateNField = privateClass1.nested.nestedField // ERROR
- }
- }
编译这个文件会产生如下输出。
- 12: error: value nestedField cannot be accessed in PrivateClass1.this.Nested
- val nestednestedNested = nested.nestedField
- ^
- 15: error: not found: value privateField1
- val field1 = privateField1
- ^
- 16: error: not found: value privateField2
- val field2 = privateField2
- ^
- 17: error: value nestedField cannot be accessed in PrivateClass2.this.Nested
- val nField = new Nested().nestedField
- ^
- 21: error: value privateField1 cannot be accessed in scopeA.PrivateClass1
- val privateField1 = privateClass1.privateField1
- ^
- 22: error: value privateField2 cannot be accessed in scopeA.PrivateClass1
- val privateField2 = privateClass1.privateField2
- ^
- 23: error: value nested cannot be accessed in scopeA.PrivateClass1
- val privateNField = privateClass1.nested.nestedField
- ^
- 7 errors found
一个private[PrivateClass1] 的成员对于其它实例来说也可见,所以equalFields 方法可以通过编译。因此,private[T] 不如private[this] 来得严格。注意,PrivateClass1 不能访问Nested.nestedField,因为那个字段被声明为private[Nested]。
提示
当T 的成员被声明为private[T] 的时候,其行为等同于private。但是它不同于private[this],后者更加严格。
如果我们修改Nested.nestedField 的范围,变成private[PrivateClass1] 会发生什么呢?让我们来看看private[T] 如何影响嵌套的类型。
- // code-examples/BasicOOP/scoping/private-type-nested-wont-compile.scala
- // WON'T COMPILE
- package scopeA {
- class PrivateClass1 {
- class Nested {
- private[PrivateClass1] val nestedField = 1
- }
- private[PrivateClass1] val nested = new Nested
- val nestednestedNested = nested.nestedField
- }
- class PrivateClass2 extends PrivateClass1 {
- val nField = new Nested().nestedField // ERROR
- }
- class PrivateClass3 {
- val privateClass1 = new PrivateClass1
- val privateNField = privateClass1.nested.nestedField // ERROR
- }
- }
编译这个文件会获得如下输出。
- 10: error: value nestedField cannot be accessed in PrivateClass2.this.Nested
- def nField = new Nested().nestedField
- ^
- 14: error: value nested cannot be accessed in scopeA.PrivateClass1
- val privateNField = privateClass1.nested.nestedField
- ^
- two errors found
现在nestedField 对PrivateClass1 来说可见,但是它对于PrivateClass1 之外来说仍然不可见。这就是private 在Java 中的工作。
让我们来用一个package 名字检查其作用范围。
- // code-examples/BasicOOP/scoping/private-pkg-type-wont-compile.scala
- // WON'T COMPILE
- package scopeA {
- private[scopeA] class PrivateClass1
- package scopeA2 {
- private [scopeA2] class PrivateClass2
- private [scopeA] class PrivateClass3
- }
- class PrivateClass4 extends PrivateClass1
- protected class PrivateClass5 extends PrivateClass1
- private class PrivateClass6 extends PrivateClass1
- private[this] class PrivateClass7 extends PrivateClass1
- private[this] class PrivateClass8 extends scopeA2.PrivateClass2 // ERROR
- private[this] class PrivateClass9 extends scopeA2.PrivateClass3
- }
- package scopeB {
- class PrivateClass1B extends scopeA.PrivateClass1 // ERROR
- }
编译这个文件会产生如下输出。
- 14: error: class PrivateClass2 cannot be accessed in package scopeA.scopeA2
- private[this] class PrivateClass8 extends scopeA2.PrivateClass2
- ^
- 19: error: class PrivateClass1 cannot be accessed in package scopeA
- class PrivateClass1B extends scopeA.PrivateClass1
- ^
- two errors found
注意PrivateClass2 无法在scopeA2 之外被继承,但是PrivateClass3 可以在scopeA 中被继承,因为它被声明为private[ScopeA]。
最后,让我们来看一下package 级别的类型成员作用域效果。
- // code-examples/BasicOOP/scoping/private-pkg-wont-compile.scala
- // WON'T COMPILE
- package scopeA {
- class PrivateClass1 {
- private[scopeA] val privateField = 1
- class Nested {
- private[scopeA] val nestedField = 1
- }
- private[scopeA] val nested = new Nested
- }
- class PrivateClass2 extends PrivateClass1 {
- val field = privateField
- val nField = new Nested().nestedField
- }
- class PrivateClass3 {
- val privateClass1 = new PrivateClass1
- val privateField = privateClass1.privateField
- val privateNField = privateClass1.nested.nestedField
- }
- package scopeA2 {
- class PrivateClass4 {
- private[scopeA2] val field1 = 1
- private[scopeA] val field2 = 2
- }
- }
- class PrivateClass5 {
- val privateClass4 = new scopeA2.PrivateClass4
- val field1 = privateClass4.field1 // ERROR
- val field2 = privateClass4.field2
- }
- }
- package scopeB {
- class PrivateClass1B extends scopeA.PrivateClass1 {
- val field1 = privateField // ERROR
- val privateClass1 = new scopeA.PrivateClass1
- val field2 = privateClass1.privateField // ERROR
- }
- }
编译这个文件会获得如下输出。
- 28: error: value field1 cannot be accessed in scopeA.scopeA2.PrivateClass4
- val field1 = privateClass4.field1
- ^
- 35: error: not found: value privateField
- val field1 = privateField
- ^
- 37: error: value privateField cannot be accessed in scopeA.PrivateClass1
- val field2 = privateClass1.privateField
- ^
- three errors found
唯一的错误是尝试从无关的package scopeB 访问scopeA 的成员,或者尝试访问属于嵌套的package scopeA2 的成员。
提示
当一个类型或成员被声明为private[P],P是一个包含它们的package, 那么这就等同于Java 的package private 可见性。
对于可见性的总结
Scala 可见性声明非常灵活,并且行为准则一致。它们为所有可能的作用域提供了细致的可见性控制,从实例级别(private[this])到package 级别(private[P])。例如,它们使得创建在顶层package 之外暴露类型的组件更加容易,而且很好得在组件package 内部隐藏了类型和类型成员的实现。
最后,我们观察到了一个潜在trait 隐藏成员的“问题”。
提示
在选择trait 成员的名字时必须小心。如果两个trait 有同样名字的成员,并且这个trait 被同一个实例使用,那么即使两个成员都是private 的也会产生命名冲突。
幸运的是,编译器会抓住这种问题。
【编辑推荐】
- Scala语言编程入门指南
- Scala编程指南 更少的字更多的事
- Scala编程指南 揭示Scala的本质
- Scala编程指南 了解Traits功能
- 51CTO专访Scala创始人:Scala拒绝学术化