您所在的位置:开发 > Java > Java+ > Scala编程指南:面向对象编程(1)

Scala编程指南:面向对象编程(1)

2010-11-17 11:31 bbsmrdj 译言网 字号:T | T
一键收藏,随时查看,分享好友!

Scala 是一种基于JVM,集合了面向对象编程和函数式编程优点的高级程序设计语言。这一章讲讲述Scala 对面向对象编程的基础支持。

AD:

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] 参数,如下所示。

  1. // code-examples/BasicOOP/type-erasure-wont-compile.scala 
  2. // WON'T COMPILE 
  3. object Foo { 
  4.   def bar(list: List[String]) = list.toString 
  5.   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编程指南 更少的字更多的事》中的“抽象类型和参数化类型”章节里看到的第一个例子的片段。

  1. // code-examples/TypeLessDoMore/abstract-types-script.scala 
  2. import java.io._ 
  3. abstract class BulkReader { 
  4.   // ... 
  5. class StringBulkReader(val source: String) extends BulkReader { 
  6.   // ... 
  7. class FileBulkReader(val source: File) extends BulkReader { 
  8.   // ... 

和在Java 一样,关键字extends 指明了父类,在这里就是BulkReader。在Scala 中,extends 也会在一个类把一个trait 作为父亲继承的时候使用(即使当它用with 关键字混入其它traits 的时候也是一样)。而且,extends 也在一个trait 是另外一个trait 或类的继承者的时候使用。是的,traits 可以继承自类。

如果你不继承任何父类,默认的父亲是AnyRef,Any 的一个直接子类。(我们会在“Scala 类型层级结构”章节中讨论Any 和AnyRef 的区别。)

Scala 构造函数

Scala 可以区分主构造函数和0个或多个辅助构造函数。在Scala 里,类的整个主体就是主构造函数。构造函数所需要的任何参数被列于类名之后。我们已经看到过很多例子了,比如我们在《第4章 - Traits》中使用的ButtonWithCallbacks 例子。

  1. // code-examples/Traits/ui/button-callbacks.scala 
  2. package ui 
  3. class ButtonWithCallbacks(val label: String, 
  4.     val clickedCallbacks: List[() => Unit]) extends Widget { 
  5.   require(clickedCallbacks != null, "Callback list can't be null!") 
  6.   def this(label: String, clickedCallback: () => Unit) = 
  7.     this(label, List(clickedCallback)) 
  8.   def this(label: String) = { 
  9.     this(label, Nil) 
  10.     println("Warning: button has no click callbacks!") 
  11.   } 
  12.   def click() = { 
  13.     // ... logic to give the appearance of clicking a physical button ... 
  14.     clickedCallbacks.foreach(f => f()) 
  15.   } 

类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 指令的作用。

  1. // code-examples/Traits/ui/button-callbacks-spec.scala 
  2. package ui 
  3. import org.specs._ 
  4. object ButtonWithCallbacksSpec extends Specification { 
  5.   "A ButtonWithCallbacks" should { 
  6.     // ... 
  7.     "not be constructable with a null callback list" in { 
  8.       val nullList:List[() => Unit] = null 
  9.       val errorMessage = 
  10.         "requirement failed: Callback list can't be null!" 
  11.       (new ButtonWithCallbacks("button1", nullList)) must throwA( 
  12.         new IllegalArgumentException(errorMessage)) 
  13.     } 
  14.   } 

Scala 甚至使得把null 作为第二个参数传给构造函数变得很困难;它不会再编译时做类型检查。然而,你向上面那样可以把null 赋给一个value。如果我们没有must throwA(...) 子句,我们会看到下面的异常被抛出。

  1. java.lang.IllegalArgumentException: requirement failed: Callback list can't be null! 
  2.         at scala.Predef$.require(Predef.scala:112) 
  3.         at ui.ButtonWithCallbacks.(button-callbacks.scala:7) 
  4. .... 

ButtonWithCallbacks 定义了两个方便用户使用的辅助构造函数。第一个辅助构造函数接受一个标签和一个单独的回调函数。它调用主构造函数,并且传递给它标签和包含了回调函数的新列表。

第二个辅助构造函数只接受一个标签。它调用主构造函数,并且传入Nil(Nil 表示了一个空的List 对象)。然后构造函数打印出一条警告消息指明没有回调函数,因为列表是不可变的,所以我们没有机会用一个新的值来替代现有的回调函数列表。

为了避免无限递归,Scala 要求每一个辅助构造函数调用在它之前定义的构造函数[ScalaSpec2009]。被调用的构造函数可以是另外一个辅助构造函数或者主构造函数,而且它必须出现在辅助构造函数主体的第一句。额外的过程可以在这个调用之后出现,比如我们例子中的打印出一个警告消息。

注意

因为所有的辅助构造函数最终都会调用主构造函数,它主体中进行的逻辑检查和其它初始化工作会在所有实例被创建的时候执行。

Scala 对构造函数的约束有一些好处。

消除重复

因为辅助构造函数会调用主构造函数,潜在的重复构造逻辑就被大大地消除了。

代码体积的减少

正如例子中所示,当一个或更多的主构造函数参数被声明为val 或者var,Scala 会自动产生一个字段,合适的存取方法(除非它们被定义为private,私有的),以及实例被创建时的初始化逻辑。

不过,这样也有至少一个缺点。

缺少弹性

有时候,迫使所有构造函数都是用同一个构造函数体并不方便。然而,我们发现这样的情况只是极少数。在这种情况下,可能是因为这个类负责了太多东西,而且应该被重构为更小的类。

调用父类构造函数

子类的主构造函数必须调用父类的一个构造函数,无论是主构造函数或者是辅助构造函数。在下面的例子里,类RadioButtonWithCallbacks 会继承ButtonWithCallbacks,并且调用ButtonWithCallbacks 的主构造函数。“Radio”按钮可以被设置为开或者关。

  1. // code-examples/BasicOOP/ui/radio-button-callbacks.scala 
  2. package ui 
  3. /** 
  4. * Button with two states, on or off, like an old-style, 
  5. * channel-selection button on a radio. 
  6. */ 
  7. class RadioButtonWithCallbacks( 
  8.   var on: Boolean, label: String, clickedCallbacks: List[() => Unit]) 
  9.       extends ButtonWithCallbacks(label, clickedCallbacks) { 
  10.   def this(on: Boolean, label: String, clickedCallback: () => Unit) = 
  11.       this(on, label, List(clickedCallback)) 
  12.   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 例子。

  1. // code-examples/BasicOOP/ui/widget.scala 
  2. package ui 
  3. abstract class Widget { 
  4.   class Properties { 
  5.     import scala.collection.immutable.HashMap 
  6.     private var values: Map[String, Any] = new HashMap 
  7.     def size = values.size 
  8.     def get(key: String) = values.get(key) 
  9.     def update(key: String, value: Any) = { 
  10.       // Do some preprocessing, e.g., filtering. 
  11.       valuesvalues = values.update(key, value) 
  12.       // Do some postprocessing. 
  13.     } 
  14.   } 
  15.   val properties = new Properties 
  16.  
  17.    

我们添加了一个Properties 类,包含了一个私有的,可变的HashMap (HashMap 本身不可变)引用。我们同时加入了3个公有方法来获取大小(例如,定义的属性个数),获取map 中的元素,以及更新map 中对应的元素等。我们可能需要在update 方法上做更多的工作,已经用注释标明。

注意

你可以从上面的例子中看到,Scala 允许在一个类中定义另外一个,或者成为“嵌套”。当你有足够多的功能需要归并到一个类里,并且这个类在仅会被外层类所使用时,一个嵌套类就非常有用。

到这里为止,我们学习了如何声明一个类,如何初始化它们,以及继承的一些基础。在下一个章节,我们会讨论类和对象内部的可见性规则。

内容导航
 第 1 页:Scala 基础面向对象编程  第 2 页:可见性规则



分享到:

热点职位

更多>>

热点专题

更多>>

读书

非常网管——网络工程案例
本书面向企业网络应用需求,详细介绍了Windows网络互联解决方案、中小企业共享上网解决方案、基于ISA Server 2006的代理服务器与

51CTO旗下网站

领先的IT技术网站 51CTO 领先的中文存储媒体 WatchStor 中国首个CIO网站 CIOage 中国首家数字医疗网站 HC3i