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 // WONT 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 cant 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 cant be null!