詳細(xì)介紹Scala面向?qū)ο缶幊?/h1>
Scala編程語言近來抓住了很多開發(fā)者的眼球。Scala 和Java, Python, Ruby, Smalltalk 以及其它類似語言一樣,是一種面向?qū)ο笳Z言。如果你來自Java 的世界,你會(huì)發(fā)現(xiàn)對(duì)Java 對(duì)象模型限制的一些顯著改進(jìn)。
類和對(duì)象的基礎(chǔ)
讓我們來回顧一下Scala OOP 的術(shù)語。
注意
我們?cè)谇懊婵吹絊cala 有聲明對(duì)象的概念,我們會(huì)在“類和對(duì)象:狀態(tài)哪里去了?”章節(jié)來討論它們。我們會(huì)使用術(shù)語實(shí)例來稱呼一個(gè)類的實(shí)例,意思是類的對(duì)象或者實(shí)例,用來避免兩者之間的混淆。
類可以用關(guān)鍵字class 來聲明。我們會(huì)在后面看到也可以加上一些其它的關(guān)鍵字,例如用final 來防止創(chuàng)建繼承類,以及用abstract 表示這個(gè)類不能被實(shí)例化,這通常是因?yàn)樗蛘呃^承了沒有具體定義的成員聲明。
一個(gè)實(shí)例可以用this 關(guān)鍵字來引用自己,這一點(diǎn)和Java 及其類似語言一樣。
遵循Scala 的約定,我們使用術(shù)語方法(method)來指代實(shí)例的函數(shù)(function)。有一些面向?qū)ο笳Z言使用術(shù)語成員函數(shù)(member function)。方法定義由def 關(guān)鍵字開始。
和Java 一樣,但是和Ruby,Python 有所區(qū)別,Scala 允許重載方法。兩個(gè)或以上的方法可以有同樣的名字,只要它們的完整簽名是唯一的。簽名包含了類型名字,參數(shù)列表及其類型,以及方法的返回值。
不過,這里有一個(gè)由類型消除引起的例外,這是一個(gè)JVM 的特性,但是被Scala 在JVM 和.NET 平臺(tái)上所利用從而最小化兼容問題。假設(shè)兩個(gè)方法其它方面都一樣,只是其中一個(gè)接受List[String] 參數(shù),而另外一個(gè)接受List[Int] 參數(shù),如下所示。
- // 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
- }
你會(huì)在第二個(gè)方法處得到一個(gè)編譯錯(cuò)誤,因?yàn)檫@兩個(gè)方法在類型消除后擁有一樣的簽名。
警告
Scala 解釋器會(huì)讓你輸入這兩個(gè)方法。它簡(jiǎn)單地拋棄了***個(gè)版本。然而,如果你嘗試用:load 文件命令去加載上面的那個(gè)例子,你會(huì)得到一樣的錯(cuò)誤。
同樣是約定,我們使用術(shù)語字段(field)來指代實(shí)例的變量。其它語言則通常使用術(shù)語屬性(attribute),例如Ruby。注意,一個(gè)實(shí)例的狀態(tài)就是該實(shí)例的字段所呈現(xiàn)的值的聯(lián)合。
正如我們所說的,只讀的(“值”)字段用val 關(guān)鍵字來聲明,可讀寫字段則用var 關(guān)鍵字來聲明。
Scala 也是允許在類中聲明類型
我們一般使用術(shù)語成員(member)來指代字段,方法或者類型。注意,字段和方法成員(除開類型成員)共享一樣的名稱空間,這一點(diǎn)和Java 不一樣。
***,引用類型的實(shí)例可以用new 關(guān)鍵字創(chuàng)建,和Java,C# 一樣。注意,你在使用默認(rèn)構(gòu)造函數(shù)時(shí)可以不用寫括號(hào)(例如,沒有參數(shù)的構(gòu)造函數(shù))。你某些情況下,字面值可以被用來替代new。例如val name = "Programming Scala" 等效于val name = new String("Programming Scala")。
值類型的實(shí)例(例如Int,Double 等),和Java 這樣的語言中的元類型相對(duì)應(yīng),永遠(yuǎn)都用字面值來創(chuàng)建。例如1,3.14 等。實(shí)際上,這些類型沒有公有構(gòu)造函數(shù),所以像val i = new Int(1) 這樣的表達(dá)式是不能編譯的。
我們會(huì)在“Scala 類型結(jié)構(gòu)”章節(jié)討論引用類型和值類型的區(qū)別。
父類
Scala 支持單繼承,不支持多繼承。一個(gè)子(或繼承的)類只可以有一個(gè)父類(基類)。唯一的例外是Scala 類層級(jí)結(jié)構(gòu)中的根,Any,沒有父類。
我們已經(jīng)見過幾個(gè)父類和子類的例子了。來看下面的例子:
- // 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 一樣,關(guān)鍵字extends 指明了父類,在這里就是BulkReader。在Scala 中,extends 也會(huì)在一個(gè)類把一個(gè)trait 作為父親繼承的時(shí)候使用(即使當(dāng)它用with 關(guān)鍵字混入其它traits 的時(shí)候也是一樣)。而且,extends 也在一個(gè)trait 是另外一個(gè)trait 或類的繼承者的時(shí)候使用。是的,traits 可以繼承自類。
如果你不繼承任何父類,默認(rèn)的父親是AnyRef,Any 的一個(gè)直接子類
#p#
Scala 構(gòu)造函數(shù)
Scala 可以區(qū)分主構(gòu)造函數(shù)和0個(gè)或多個(gè)輔助構(gòu)造函數(shù)。在Scala 里,類的整個(gè)主體就是主構(gòu)造函數(shù)。構(gòu)造函數(shù)所需要的任何參數(shù)被列于類名之后。我們已經(jīng)看到過很多例子了,來看下面的例子。
- // 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 表示了圖形用戶界面上的一個(gè)按鈕。它有一個(gè)標(biāo)簽和一個(gè)回調(diào)函數(shù)的列表,這些函數(shù)會(huì)在按鈕被點(diǎn)擊的時(shí)候被調(diào)用。每一個(gè)回調(diào)函數(shù)都不接受參數(shù),并且返回Unit。方法click 會(huì)遍歷回調(diào)函數(shù)的列表,然后一個(gè)個(gè)地調(diào)用它們。
ButtonWithCallbacks 定義了3個(gè)構(gòu)造函數(shù)。主構(gòu)造函數(shù),類的主題,有一個(gè)參數(shù)列表來接受標(biāo)簽字符串和回調(diào)函數(shù)的列表。因?yàn)槊恳粋€(gè)參數(shù)都被聲明為val, 編譯器為每一個(gè)參數(shù)都生成一個(gè)私有字段(會(huì)使用一個(gè)不同的內(nèi)部名稱),以及名字和參數(shù)一致的公有讀取方法。“私有”和“公有”在這里的意思和在大多數(shù)面向?qū)ο笳Z言里一樣。我們會(huì)在下面的“可見性規(guī)則”章節(jié)討論不同的可見性規(guī)則和控制它們的關(guān)鍵字。
如果參數(shù)有一個(gè)var 關(guān)鍵字,一個(gè)公有的寫方法會(huì)被自動(dòng)生成,并且名字為參數(shù)名加下劃線等號(hào)(_=)。例如,如果label 被聲明為var, 對(duì)應(yīng)的寫方法則為label_=,而且它會(huì)接受一個(gè)字符串作為參數(shù)。
有時(shí)候你可能不希望自動(dòng)生成這些訪問器方法。換句話說,你希望字段是私有的。在val 或者var 之前加上private 關(guān)鍵字,訪問器方法就不會(huì)被生成
注意
對(duì)于Java 程序員,Scala 沒有遵循s [JavaBeanSpec] 約定 - 字段讀取、寫方法分別對(duì)應(yīng)get 和set 的前綴,緊接著是***個(gè)字母大寫的字段名。我們會(huì)在“當(dāng)方法和字段存取器無法區(qū)分時(shí):唯一存取的原則”章節(jié)中討論唯一存取原則時(shí)看到原因。不過,你可以在需要時(shí)通過scala.reflect.BeanProperty 來獲得JavaBeans 風(fēng)格的訪問器。
當(dāng)類的一個(gè)實(shí)例被創(chuàng)建時(shí),每一個(gè)參數(shù)對(duì)應(yīng)的字段都會(huì)被參數(shù)自動(dòng)初始化。初始化這些字段不需要邏輯上的構(gòu)造函數(shù),這和很多面向?qū)ο笳Z言不同。
ButtonWithCallbacks 類主體(換言之,構(gòu)造函數(shù))的***個(gè)指令是一個(gè)保證被傳入構(gòu)造函數(shù)的參數(shù)列表是一個(gè)非空列表的測(cè)試。(不過它確實(shí)允許一個(gè)空的Nil 列表。)它使用了方便的require 函數(shù),這個(gè)函數(shù)是被自動(dòng)導(dǎo)入到當(dāng)前的作用域中的。如果這個(gè)列表是null, require 會(huì)拋出一個(gè)異常。require 函數(shù)和它對(duì)應(yīng)的假設(shè)對(duì)于設(shè)計(jì)契約式程序非常有用。
這里是ButtonWithCallbacks 的完整Specification(規(guī)格)的一部分,它展示了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 作為第二個(gè)參數(shù)傳給構(gòu)造函數(shù)變得很困難;它不會(huì)再編譯時(shí)做類型檢查。然而,你向上面那樣可以把null 賦給一個(gè)value。如果我們沒有must throwA(...) 子句,我們會(huì)看到下面的異常被拋出。
- 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 定義了兩個(gè)方便用戶使用的輔助構(gòu)造函數(shù)。***個(gè)輔助構(gòu)造函數(shù)接受一個(gè)標(biāo)簽和一個(gè)單獨(dú)的回調(diào)函數(shù)。它調(diào)用主構(gòu)造函數(shù),并且傳遞給它標(biāo)簽和包含了回調(diào)函數(shù)的新列表。
第二個(gè)輔助構(gòu)造函數(shù)只接受一個(gè)標(biāo)簽。它調(diào)用主構(gòu)造函數(shù),并且傳入Nil(Nil 表示了一個(gè)空的List 對(duì)象)。然后構(gòu)造函數(shù)打印出一條警告消息指明沒有回調(diào)函數(shù),因?yàn)榱斜硎遣豢勺兊?,所以我們沒有機(jī)會(huì)用一個(gè)新的值來替代現(xiàn)有的回調(diào)函數(shù)列表。
為了避免無限遞歸,Scala 要求每一個(gè)輔助構(gòu)造函數(shù)調(diào)用在它之前定義的構(gòu)造函數(shù)[ScalaSpec2009]。被調(diào)用的構(gòu)造函數(shù)可以是另外一個(gè)輔助構(gòu)造函數(shù)或者主構(gòu)造函數(shù),而且它必須出現(xiàn)在輔助構(gòu)造函數(shù)主體的***句。額外的過程可以在這個(gè)調(diào)用之后出現(xiàn),比如我們例子中的打印出一個(gè)警告消息。
注意
因?yàn)樗械妮o助構(gòu)造函數(shù)最終都會(huì)調(diào)用主構(gòu)造函數(shù),它主體中進(jìn)行的邏輯檢查和其它初始化工作會(huì)在所有實(shí)例被創(chuàng)建的時(shí)候執(zhí)行。
#p#
Scala 對(duì)構(gòu)造函數(shù)的約束有一些好處。
消除重復(fù)
因?yàn)檩o助構(gòu)造函數(shù)會(huì)調(diào)用主構(gòu)造函數(shù),潛在的重復(fù)構(gòu)造邏輯就被大大地消除了。
代碼體積的減少
正如例子中所示,當(dāng)一個(gè)或更多的主構(gòu)造函數(shù)參數(shù)被聲明為val 或者var,Scala 會(huì)自動(dòng)產(chǎn)生一個(gè)字段,合適的存取方法(除非它們被定義為private,私有的),以及實(shí)例被創(chuàng)建時(shí)的初始化邏輯。
不過,這樣也有至少一個(gè)缺點(diǎn)。
缺少彈性
有時(shí)候,迫使所有構(gòu)造函數(shù)都是用同一個(gè)構(gòu)造函數(shù)體并不方便。然而,我們發(fā)現(xiàn)這樣的情況只是極少數(shù)。在這種情況下,可能是因?yàn)檫@個(gè)類負(fù)責(zé)了太多東西,而且應(yīng)該被重構(gòu)為更小的類。
調(diào)用父類構(gòu)造函數(shù)
子類的主構(gòu)造函數(shù)必須調(diào)用父類的一個(gè)構(gòu)造函數(shù),無論是主構(gòu)造函數(shù)或者是輔助構(gòu)造函數(shù)。在下面的例子里,類RadioButtonWithCallbacks 會(huì)繼承ButtonWithCallbacks,并且調(diào)用ButtonWithCallbacks 的主構(gòu)造函數(shù)。“Radio”按鈕可以被設(shè)置為開或者關(guān)。
- // 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 的主構(gòu)造函數(shù)接受3個(gè)參數(shù),一個(gè)開關(guān)狀態(tài)(真或假),一個(gè)標(biāo)簽,以及一個(gè)回調(diào)函數(shù)例表。它把標(biāo)簽和回調(diào)函數(shù)列表傳給父類ButtonWithCallbacks。開關(guān)狀態(tài)參數(shù)(on)被聲明為var,所以是可變的。on 也是每一個(gè)單選按鈕的私有屬性。 為了和父類保持統(tǒng)一,RadioButtonWithCallbacks 還定義了兩個(gè)輔助構(gòu)造函數(shù)。注意它們必須調(diào)用一個(gè)之前定義的構(gòu)造函數(shù),和之前一樣。它們不能直接調(diào)用ButtonWithCallbacks 的構(gòu)造函數(shù)。
注意
雖然和Java 一樣,super 關(guān)鍵字通常被用來調(diào)用重寫的方法,但是它不能被用作調(diào)用父類的構(gòu)造函數(shù)。
嵌套類
Scala 和許多面向?qū)ο笳Z言一樣,允許你嵌套聲明類。假設(shè)我們希望所有的部件都有一系列的屬性。這些屬性可以是大小,顏色,是否可見等。我們可以使用一個(gè)簡(jiǎn)單的map 來保存這些屬性,但是我們假設(shè)還希望能夠控制對(duì)這些屬性的存取,并且當(dāng)它們改變時(shí)能進(jìn)行一些其它的操作。
下面的例子:
- // 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
- }
我們添加了一個(gè)Properties 類,包含了一個(gè)私有的,可變的HashMap (HashMap 本身不可變)引用。我們同時(shí)加入了3個(gè)公有方法來獲取大?。ɡ?,定義的屬性個(gè)數(shù)),獲取map 中的元素,以及更新map 中對(duì)應(yīng)的元素等。我們可能需要在update 方法上做更多的工作,已經(jīng)用注釋標(biāo)明。
注意
你可以從上面的例子中看到, 允許在一個(gè)類中定義另外一個(gè),或者成為“嵌套”。當(dāng)你有足夠多的功能需要?dú)w并到一個(gè)類里,并且這個(gè)類在僅會(huì)被外層類所使用時(shí),一個(gè)嵌套類就非常有用。
到這里為止,我們學(xué)習(xí)了如何聲明一個(gè)類,如何初始化它們,以及繼承的一些基礎(chǔ)。希望對(duì)你有幫助。
【編輯推薦】