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章 - 应用程序设计》的“用契约式设计方式构造更佳的设计”章节中讨论这个问题。