歡迎您光臨本站 註冊首頁

Scala編程指南:面向對象編程(1)

←手機掃碼閱讀     火星人 @ 2014-03-09 , reply:0

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 允許在一個類中定義另外一個,或者成為「嵌套」.當你有足夠多的功能需要歸併到一個類里,並且這個類在僅會被外層類所使用時,一個嵌套類就非常有用.
到這裡為止,我們學習了如何聲明一個類,如何初始化它們,以及繼承的一些基礎.在下一個章節,我們會討論類和對象內部的可見性規則.


[火星人 ] Scala編程指南:面向對象編程(1)已經有584次圍觀

http://coctec.com/docs/java/show-post-60099.html