Scala學習(三)函數式對象

來源:互聯網
上載者:User
from Scala嚮導第1章函數式對象

有了從前幾章獲得的Scala基礎知識,你已經為探索如何在Scala裡設計出更全面特徵的對象做好了準備。本章的重點在於定義函數式對象,也就是說,沒有任何可變狀態的對象的類。作為啟動並執行例子,我們將創造若干把分數作為不可變對象建模的類的變體。在這過程中,我們會展示給你Scala物件導向編程的更多方面:類參數和建構函式,方法和操作符,私人成員,子類方法重載,先決條件檢查,同類方法重載和自指向。

1.1 類Rational的式樣書

分數:rational number是一種可以表達為比率的數字,這裡的和是數字,其中不能為零。被稱作是分子:numerator,被稱作是分母:denominator。分數的例子有:,,和。與浮點數相比較,分數的優勢是小數部分得到了完全表達,沒有舍入或估算。

本章我們將要設計的類必須模型化分數的行為,包括允許它們執行加,減,乘還有除運算。要加兩個分數,首先要獲得公分母,然後才能把兩個分子相加。例如,要計算,先把左運算元的上下部分都乘上3,右運算元的兩部分都乘上2,得到了。把兩個分子相加產生結果,。要乘兩個分數,可以簡單的兩個分子相乘,然後兩個分母相乘。因此,得到了,還可以簡化表示成它的“通常”形式。除法是把右運算元分子分母調換,然後做乘法。例如與相同,結果是。

一個,或許不怎麼重要的,發現是數學上,分數不具有可變的狀態。一個分數加到另外一個分數上,產生的結果是一個新的分數。而原來的數不會被“改變”。我們將在本章設計的不可變的Rational類將秉承這一屬性每個分數將都被表示成一個Rational對象。當兩個Rational對象相加時,一個新的帶著累加結果的Rational對象將被建立出來

本章還將捎帶提一些Scala讓你寫出感覺像原生語言支援的庫的方法。例如,在本章結尾你將能用Rational類這樣做:

scala> val oneHalf = new Rational(1, 2)

oneHalf: Rational = 1/2

scala> val twoThirds = new Rational(2, 3)

twoThirds: Rational = 2/3

scala> (oneHalf / 7) + (1 twoThirds)

res0: Rational = 17/42

1.2 建立Rational

開始設計Rational類的著手點是考慮客戶程式員將如何建立一個新的Rational對象。假設我們已決定讓Rational對象是不可變的,我們將需要那個客戶在建立執行個體時提供所有需要的資料(本例中,是分子和分母)。因此,我們應該這麼開始設計:

class Rational(n: Int, d: Int)

這行代碼裡首先應當注意到的是如果類沒有主體,就不需要指定一對空的大括弧(當然你如果想的話也可以)。在類名,Rational,之後括弧裡的n和d,被稱為類參數:class parameter。Scala編譯器會收集這兩個類參數並創造一個帶同樣的兩個參數的主構造器:primary constructor

不可變對象的權衡

不可變對象提供了若干強於可變對象的優點和一個潛在的缺點首先,不可變對象常常比可變對象更具邏輯性,因為它們沒有隨著時間而變化的複雜的狀態空間。其次,你可以很自由地傳遞不可變對象,而或許需要在把可變對象傳遞給其它代碼之前,需要先建造個以防萬一的副本。第三,沒有機會能讓兩個同時訪問不可變對象的線程破壞它合理構造的狀態,因為根本沒有線程可以改變不可變對象的狀態。第四,不可變對象讓雜湊表索引值更安全。比方說,如果可變對象在被放進了HashSet之後被改變,那麼你下一次尋找這個HashSet就找不到這個對象了。

不可變對象唯一的缺點就是它們有時需要複製很大的對象圖而可變對象的更新可以在原地發生。有些情況下這會變得難以快速完成而可能產生效能瓶頸。結果,要求庫提供可變替代以使其更容易在大資料結構的中間改變一些元素也並非是一件稀奇的事情。例如,類StringBuilder是不可變的String的可變替代。第十八章中我們會給出更多Scala裡設計可變對象的細節。

注意

這個最初的Rational例子凸顯了Java和Scala之間的不同。Java類具有可以帶參數的構造器,而Scala類可以直接帶參數。Scala的寫法更簡潔——類參數可以直接在類的主體中使用;沒必要定義欄位然後寫賦值函數把構造器的參數複製到欄位裡。這可以潛在地節省很多固定寫法,尤其是對小類來說。

Scala編譯器將把你放在類內部的任何不是欄位的部分或者方法定義的代碼,編譯進主構造器。例如,你可以像這樣列印輸出一條除錯訊息:

class Rational(n: Int, d: Int) {

println("Created "+n+"/"+d)

}

根據這個代碼,Scala編譯器將把println調用放在Rational的主構造器。因此,println調用將在每次建立一個新的Rational執行個體時列印這條除錯資訊:

scala> new Rational(1, 2)

Created 1/2

res0: Rational = Rational@a0b0f5

1.3 重新實現toString方法

前例中當Rational執行個體被建立之後,解譯器列印輸出“Rational@a0b0f5”。解譯器是通過調用Rational對象的toString方法獲得的這個看上去有些好玩兒的字串。預設情況下,Rational類繼承了定義在java.lang.Object類上的toString實現,只是列印類名,一個@符號和一個十六進位數。toString的結果主要是想通過提供可以用在除錯時的語句列印,日誌訊息,測試錯誤報表和解譯器,除錯器輸出的資訊來嘗試對程式員提供協助。目前toString提供的結果不會特別有用,因為它沒有給出任何它被調用的Rational數值的任何線索。更有用的toString實現應該列印出Rational的分子和分母。你可以通過在Rational類裡增加toString方法的方式重載:override預設的實現,如:

class Rational(n: Int, d: Int) {

override def toString = n +"/"+ d

}

方法定義前的override修飾符標示了之前的方法定義被重載;第10章會更進一步說明。現在分數顯示得很漂亮了,所以我們去掉了前一個版本的Rational類裡面的println除錯語句。你可以在解譯器裡測試Rational的新行為:

scala> val x = new Rational(1, 3)

x: Rational = 1/3

scala> val y = new Rational(5, 7)

y: Rational = 5/7

1.4 檢查先決條件

下一步,我們將把視線轉向當前主構造器行為裡的一些問題。如本章早些時候提到的,分數的分母不能為零。然而目前主構造器會接受把零傳遞給d:

scala> new Rational(5, 0)

res6: Rational = 5/0

物件導向編程的一個優點就是它允許你把資料封裝在對象之內以便於你確保資料在整個生命週期中是有效。像Rational這樣的不可變對象,這就意味著你必須確保在對象建立的時候資料是有效(並且,確保對象的確是不可變的,這樣資料就不會在之後變成無效的狀態)。由於零做分母對Rational來說是無效狀態,因此在把零傳遞給d的時候,務必不能讓Rational被構建出來。

解決這個問題的最好辦法是為主構造器定義一個先決條件:precondition說明d必須為非零值。先決條件是對傳遞給方法或構造器的值的限制,是調用者必須滿足的需求。一種方式是使用require方法,[1]如:

class Rational(n: Int, d: Int) {

require(d != 0)

override def toString = n +"/"+ d

}

require方法帶一個布爾型參數。如果傳入的值為真,require將正常返回。反之,require將通過拋出IllegalArgumentException來阻止對象被構造。

1.5 添加欄位

現在主構造器可以正確地執行先決條件,我們將把注意力集中到支援加法。想做到這點,我們將在類Rational上定義一個公開的add方法,它帶另一個Rational做參數。為了保持Rational不可變,add方法必須不能把傳入的分數加到自己身上。而是必須建立並返回一個全新的帶有累加值的Rational。你或許想你可以這麼寫add:

class Rational(n: Int, d: Int) { // 編譯不過

require(d != 0)

override def toString = n +"/"+ d

def add(that: Rational): Rational =

new Rational(n * that.d + that.n * d, d * that.d)

}

很不幸,上面的代碼會讓編譯器提示說:

<console>:11: error: value d is not a member of Rational

new Rational(n * that.d + that.n * d, d * that.d)

ˆ

<console>:11: error: value d is not a member of Rational

new Rational(n * that.d + that.n * d, d * that.d)

ˆ

儘管類參數n和d都在你的add代碼可引用的範圍內,但是在調用add的對象中僅能訪問它們的值。因此,當你在add的實現裡講n或d的時候,編譯器將很高興地提供給你這些類參數的值。但絕對不會讓你使用that.n或that.d,因為that並不指向add被調用的Rational對象。[2]要想訪問that的n和d,需要把它們放在欄位中。代碼6.1展示了如何把這些欄位加入類Rational。[3]

在代碼6.1展示的Rational版本裡,我們增加了兩個欄位,分別是numer和denom,並用類參數n和d初始化它們。[4]我們還改變了toString和add的實現,讓它們使用欄位,而不是類參數。類Rational的這個版本能夠編譯通過,可以通過分數的加法測試它:

class Rational(n: Int, d: Int) {

require(d != 0)

val numer: Int = n

val denom: Int = d

override def toString = numer+"/"+denom

def add(that: Rational): Rational =

new Rational(

numer * that.denom + that.numer * denom,

denom * that.denom

)

}

代碼 6.1 帶欄位的Rational

scala> val oneHalf = new Rational(1, 2)

oneHalf: Rational = 1/2

scala> val twoThirds = new Rational(2, 3)

twoThirds: Rational = 2/3

scala> oneHalf add twoThirds

res0: Rational = 7/6

另一件之前不能而現在可以做的事是在對象外面訪問分子和分母。只要訪問公用的numer和denom欄位即可:

scala> val r = new Rational(1, 2)

r: Rational = 1 / 2

scala> r.numer

res7: Int = 1

scala> r.denom

res8: Int = 2

1.6 自指向

關鍵字this指向當前執行方法被調用的對象執行個體,或者如果使用在構造器裡的話,就是正被構建的對象執行個體。例如,我們考慮添加一個方法,lessThan,來測試給定的分數是否小於傳入的參數:

def lessThan(that: Rational) =

this.numer * that.denom < that.numer * this.denom

這裡,this.numer指向lessThan被調用的那個對象的分子。你也可以去掉this首碼而只是寫numer;著兩種寫法是相同的。

舉一個不能缺少this的例子,考慮在Rational類裡添加max方法返回指定分數和參數中的較大者:

def max(that: Rational) =

if (this.lessThan(that)) that else this

這裡,第一個this是冗餘的,你寫成(lessThan(that))也是一樣的。但第二個this表示了當測試為假的時候的方法的結果;如果你省略它,就什麼都返回不了了。

1.7 從構造器

有些時候一個類裡需要多個構造器。Scala裡主構造器之外的構造器被稱為從構造器:auxiliary constructor。比方說,分母為1的分數唯寫分子的話就更為簡潔。如,對於來說,可以只是寫成5。因此,如果不是寫成Rational(5, 1),客戶程式員簡單地寫成Rational(5)或許會更好看一些。這就需要給Rational添加一個只帶一個參數,分子,的從構造器並預先設定分母為1。代碼6.2展示了應該有的樣子。

class Rational(n: Int, d: Int) {

require(d != 0)

val numer: Int = n

val denom: Int = d

def this(n: Int) = this(n, 1)

override def toString = numer+"/"+denom

def add(that: Rational): Rational =

new Rational(

numer * that.denom + that.numer * denom,

denom * that.denom

)

}

代碼 6.2 帶有從構造器的Rational

Scala的從構造器開始於def this(...)。Rational的從構造器主體幾乎完全是調用主構造器,直接傳遞了它的唯一的參數,n,作為分子和1作為分母。輸入下列代碼到解譯器裡可以實際看到從構造器的效果:

scala> val y = new Rational(3)

y: Rational = 3/1

Scala裡的每一個從構造器的第一個動作都是調用同一個類裡面其他的構造器。換句話說就是,每個Scala類裡的每個從構造器都是以“this(...)”形式開頭的。被調用的構造器既可以是主構造器(好像Rational這個例子),也可以是從文本上來看早於調用構造器的其它從構造器。這個規則的根本結果就是每一個Scala的構造器調用終將結束於對類的主構造器的調用。因此主構造器是類的唯一進入點。

注意

若你熟悉Java,你或許會奇怪為什麼Scala構造器的規矩比Java的還要大。Java裡,構造器的第一個動作必須要麼調用同類裡的另一個構造器,要麼直接調用超類的構造器。Scala的類裡面,只有主構造器可以調用超類的構造器。Scala裡更嚴格的限制實際上是權衡了更高的簡潔度和與Java構造器相比的簡易性所付出的代價之後作出的設計。超類,構造器調用和繼承互動的細節將在第10章裡解釋。

1.8 私人欄位和方法

上一個版本的Rational裡,我們只是分別用n初始化了numer,用d初始化了denom。結果,Rational的分子和分母可能比它所需要的要大。例如分數,可以更約簡化為相同的最簡形式,,但Rational的主構造器當前並不做這個工作:

scala> new Rational(66, 42)

res15: Rational = 66/42

要想對分數進行約簡化,需要把分子和分母都除以最大公約數:greatest common divisor。如:66和42的最大公約數是6。(另一種說法就是,6是能夠除盡66和42的最大的整數。)的分子和分母都除以6就產生它的最簡形式,。代碼6.3展示了如何做到這點:

class Rational(n: Int, d: Int) {

require(d != 0)

private val g = gcd(n.abs, d.abs)

val numer = n / g

val denom = d / g

def this(n: Int) = this(n, 1)

def add(that: Rational): Rational =

new Rational(

numer * that.denom + that.numer * denom,

denom * that.denom

)

override def toString = numer+"/"+denom

private def gcd(a: Int, b: Int): Int =

if (b == 0) a else gcd(b, a % b)

}

代碼 6.3 帶私人欄位和方法的Rational

這個版本的Rational裡,我們添加了私人欄位,g,並修改了numer和denom的初始化器(初始化器:initializer是初始設定變數,例如初始化numer的“n / g”,的代碼)。因為g是私人的,它只能在類的主體之內,而不能在外部被訪問。我們還添加了一個私人方法,gcd,用來計算傳入的兩個Int的最大公約數。比方說,gcd(12, 8)是4。正如你在4.1節中看到的,想讓一個欄位或方法私人化你只要把private關鍵字放在定義的前面。私人的“助手方法”gcd的目的是把類的其它部分,這裡是主構造器,需要的代碼分離出來。為了確保g始終是正的,我們傳入n和d的絕對值,調用abs即可獲得任意整數的絕對值。

Scala編譯器將把Rational的三個欄位的初始化代碼依照它們在原始碼中出現的次序放入主構造器。所以g的初始化代碼,gcd(n.abs, d.abs),將在另外兩個之前執行,因為它在源檔案中出現得最早。g將被初始化為類參數,n和d,的絕對值的最大公約數。然後再被用於numer和denom的初始化。通過把n和d整除它們的最大公約數,g,每個Rational都將被構造成它的最簡形式:

scala> new Rational(66, 42)

res24: Rational = 11/7

1.9 定義操作符

Rational加法的當前實現僅就完成功能來講是沒問題的,但它可以做得更好用。你或許會問你自己為什麼對於整數或浮點數你可以寫成:

x + y

但是如果是分數就必須寫成:

x.add(y)

或至少是:

x add y

沒有合理的解釋為什麼就必須是這樣的。分數和別的數應該是一樣的。數學的角度上看他們甚至比,唔,浮點數,更自然。為什麼就不能使用自然的數學操作符呢?Scala裡面你做得到。本章後續部分,我們會告訴你怎麼做。

第一步是用通常的數學的符號替換add方法。這可以直接做到,因為Scala裡+是合法的標識符。我們可以用+定義方法名。既然已經到這兒了,你可以同樣實現一個*方法以實現乘法,結果展示在代碼6.4中:

class Rational(n: Int, d: Int) {

require(d != 0)

private val g = gcd(n.abs, d.abs)

val numer = n / g

val denom = d / g

def this(n: Int) = this(n, 1)

def +(that: Rational): Rational =

new Rational(

numer * that.denom + that.numer * denom,

denom * that.denom

)

def *(that: Rational): Rational =

new Rational(numer * that.numer, denom * that.denom)

override def toString = numer+"/"+denom

private def gcd(a: Int, b: Int): Int =

if (b == 0) a else gcd(b, a % b)

}

代碼 6.4 帶操作符方法的Rational

有了這種方式定義的Rational類,你現在可以這麼寫了:

scala> val x = new Rational(1, 2)

x: Rational = 1/2

scala> val y = new Rational(2, 3)

y: Rational = 2/3

scala> x + y

res32: Rational = 7/6

與以往一樣,在最後輸入的那行裡的文法格式相等於一個方法調用。你也能這麼寫:

scala> x.+(y)

res33: Rational = 7/6

不過這樣寫可讀性不佳。

另外一件要提的是基於5.8節中提到的Scala的操作符優先順序規則,Rational裡面的*方法要比+方法綁定得更結實。或者說,Rational涉及到+和*操作的運算式會按照預期的方式那樣表現。例如,x + x * y會當作x + (x * y)而不是(x + x) * y:

scala> x + x * y

res34: Rational = 5/6

scala> (x + x) * y

res35: Rational = 2/3

scala> x + (x * y)

res36: Rational = 5/6

1.10 Scala的標識符

現在你已經看到了Scala裡兩種構成標識符的方式:字母數字式和操作符。Scala在構成標識符方面有非常靈活的規則。除了這兩種之外你會看到還有其它的兩種。本節將說明所有的這四種標識符構成方式。

字母數位識別碼符:alphanumeric identifier起始於一個字母或底線,之後可以跟字母,數字,或底線。‘$’字元也被當作是字母,但是被保留作為Scala編譯器產生的標識符之用。使用者程式裡的標識符不應該包含‘$’字元,儘管能夠編譯通過;但是這樣做有可能導致與Scala編譯器產生的標識符發生名稱衝撞。

Scala遵循Java的駝峰式[5]標識符習俗,例如toString和HashSet。儘管底線在標識符內是合法的,但在Scala程式裡並不常用,部分原因是為了保持與Java一致,同樣也由於底線在Scala代碼裡有許多其它非標識符用法。因此,最好避免使用像to_string,__init__,或name_這樣的標識符。欄位,方法參數,本地變數,還有函數的駝峰式名稱,應該以小寫字母開始,如:length,flatMap,還有s。類和特質的駝峰式名稱應該以大寫字母開始,如:BigInt,List,還有UnbalancedTreeMap。[6]

注意

標識符結尾使用底線的一個結果就是,比如說,如果你嘗試寫一個這樣的定義,“val name_: Int = 1”,你會收到一個編譯器錯誤。編譯器會認為你正常是定義一個叫做“name_:”的變數。要讓它編譯通過,你將需要在冒號之前插入一個額外的空格,如:“val name_ : Int = 1”。

Scala與Java的習慣不一致的地方在於常量名。Scala裡,constant這個詞並不等同於val。儘管val在被初始化之後的確保持不變,但它還是個變數。比方說,方法參數是val,但是每次方法被調用的時候這些val都可以代表不同的值。而常量更持久。比方說,scala.Math.Pi被定義為很接近實數p的雙精確度值,表示圓周和它的直徑的比值。這個值不太可能改變,因此Pi顯然是個常量。你還可以用常數去給一些你代碼裡作為幻數:magic number要用到的值一個名字:文本值不具備解釋能力,如果出現在多個地方將會變得極度糟糕。你還可能會需要定義用在模式比對裡的常量,用例將在15.2節中說明。Java裡,習慣上常量名全都是大寫的,用底線分隔單詞,如MAX_VALUE或PI。Scala裡,習慣只是第一個字母必須大寫。因此,Java風格的常量名,如X_OFFSET,在Scala裡也可以用,但是Scala的慣例是常數也用駝峰式風格,如XOffset

操作符標識符:operator identifier由一個或多個操作符字元組成。操作符字元是一些如+,:,?,~或#的可列印的ASCII字元。[7]以下是一些操作符標識符的例子:

+ ++ ::: <?> :->

Scala編譯器將內部“粉碎”操作符標識符以轉換成合法的內嵌‘$’的Java標識符。例如,標識符:->將被內部表達為$colon$minus$greater。若你想從Java代碼訪問這個標識符,就應使用這個內部表達。

Scala裡的操作符標識符可以變得任意長,因此在Java和Scala間有一些小差別。Java裡,輸入x<-y將會被拆分成四個詞彙符號,所以寫成x < - y也沒什麼不同。Scala裡,<-將被作為一個標識符拆分,而得到x <- y。如果你想要得到第一種解釋,你要在‘<’和‘-’字元間加一個空格。這大概不會是實際應用中的問題,因為沒什麼人會在Java裡寫x<-y的時候不注意加空格或括弧的。

混合標識符:mixed identifier由字母數字組成,後面跟著底線和一個操作符標識符。例如,unary_+被用做定義一元的‘+’操作符的方法名。或者,myvar_=被用做定義賦值操作符的方法名。多說一句,混合標識符格式myvar_=是由Scala編譯器產生的用來支援屬性:property的;第十八章進一步說明。

文本標識符:literal identifier是用反引號`...`包括的任意字串。如:

`x` `<clinit>` `yield`

它的思路是你可以把任何運行時認可的字串放在反引號之間當作標識符。結果總是Scala標識符。即使包含在反引號間的名稱是Scala保留字,這個規則也是有效。在Java的Thread類中訪問靜態yield方法是其典型的用例。你不能寫Thread.yield()因為yield是Scala的保留字。然而,你仍可以在反引號裡引用方法的名稱,例如Thread.`yield`()。

1.11 方法重載

回到類Rational上來。在最近一次改變之後,你可以在分數上用自然的風格做加法和乘法。但別忘了還有混合運算。例如,你不能把一個分數和一個整數乘在一起,因為‘*’的運算元只能是分數。所以對於分數r你不能寫r * 2。而必須寫成r * new Rational(2),看上去不漂亮。為了讓Rational用起來更方便,可以在類上增加能夠執行分數和整數之間的加法和乘法的新方法。既然已經到這裡了,還可以再加上減法和除法。結果展示在代碼6.5中:

class Rational(n: Int, d: Int) {

require(d != 0)

private val g = gcd(n.abs, d.abs)

val numer = n / g

val denom = d / g

def this(n: Int) = this(n, 1)

def +(that: Rational): Rational =

new Rational(

numer * that.denom + that.numer * denom,

denom * that.denom

)

def +(i: Int): Rational =

new Rational(numer + i * denom, denom)

def -(that: Rational): Rational =

new Rational(

numer * that.denom - that.numer * denom,

denom * that.denom

)

def -(i: Int): Rational =

new Rational(numer - i* denom, denom)

def *(that: Rational): Rational =

new Rational(numer * that.numer, denom * that.denom)

def *(i: Int): Rational =

new Rational(numer * i, denom)

def /(that: Rational): Rational =

new Rational(numer * that.denom, denom * that.numer)

def /(i: Int): Rational =

new Rational(numer, denom * i)

override def toString = numer+"/"+denom

private def gcd(a: Int, b: Int): Int =

if (b == 0) a else gcd(b, a % b)

}

代碼 6.5 含有重載方法的Rational

現在每種數學方法都有兩個版本了:一個帶分數做參數,另一個帶整數。或者可以說,這些方法名都被重載:overload了,因為每個名字現在都被多個方法使用。例如,+這個名字被一個帶Rational的和另一個帶Int的方法使用。方法調用裡,編譯器會揀出正確地匹配了參數類型的重載方法版本。例如,如果x.+(y)的參數y是Rational,編譯器就會揀帶有Rational參數的+方法來用。相反如果參數是整數,編譯器就會揀帶有Int參數的+方法做替代。如果你嘗試輸入:

scala> val x = new Rational(2, 3)

x: Rational = 2/3

scala> x * x

res37: Rational = 4/9

scala> x * 2

res38: Rational = 4/3

你會看到*方法的調用取決於每個例子裡面右側運算元的類型。

注意

Scala分辨重載方法的過程與Java極為相似。任何情況下,被選中的重載版本都是最符合參數靜態類型的那個。有時如果不止一個最符合的版本;這種情況下編譯器會給你一個“參考模糊”的錯誤

1.12 隱式轉換

現在你能寫r * 2了,或許你想交換運算元,就像2 * r這樣。不幸的是這樣做還不可以:

scala> 2 * r

<console>:7: error: overloaded method value * with alternatives

(Double)Double <and> (Float)Float <and> (Long)Long <and> (Int)Int

<and> (Char)Int <and> (Short)Int <and> (Byte)Int cannot be

applied to (Rational)

val res2 = 2 * r

ˆ

這裡的問題是2 * r等同於2.*(r),因此這是在整數2上的方法調用。但Int類沒有帶Rational參數的乘法——沒辦法,因為類Rational不是Scala庫的標準類。

然而,Scala裡有另外一種方法解決這個問題:你可以建立一個在需要的時候能自動把整數轉換為分數的隱式轉換。試著把這行代碼加入到解譯器:

scala> implicit def intToRational(x: Int) = new Rational(x)

這行代碼定義了從Int到Rational的轉換方法。方法前面的implicit修飾符告訴編譯器若干情況下自動調用它。定義了轉換之後,你現在可以重試之前失敗的例子了:

scala> val r = new Rational(2,3)

r: Rational = 2/3

scala> 2 * r

res0: Rational = 4/3

請注意隱式轉換要起作用,需要定義在作用範圍之內。如果你把隱式方法定義放在類Rational之內,它就不在解譯器的作用範圍。現在,你要在解譯器內直接定義它。

正如你在這個例子中能領略到的,隱式轉換是把庫變得更靈活和更方便的非常強大的技術。因為他們如此強大,所以也很容易被誤用。第二十一章裡你將發現隱式轉換的更多細節,包括在需要的時候把它們帶入作用範圍的方式。

譯者註:

實際上如果定義了隱式轉換,前面的Rational類甚至可以不用定義兩套方法:僅定義對Rational的操作即可,隱式轉換會自動把運算元轉換為Rational類的執行個體。

1.13 一句警告

如本章所示範的,用操作符名稱來建立方法並定義隱式轉換能協助你設計出讓客戶代碼更簡潔和易於理解的庫。Scala給了你大量的設計這種便於使用庫的能力,不過請牢記能力帶來的責任。

如果無技巧性地使用,操作符方法和隱式轉換都會讓客戶代碼變得難以閱讀和理解。因為隱式轉換是由編譯器隱式地應用的,而不是顯式地寫在原始碼中,對於客戶程式員來說哪個隱式轉換被應用了並非顯而易見。而且儘管操作符方法通常會使得客戶代碼更簡潔,但它只會在客戶程式員能夠識別和記住每個操作符的意思的程度上讓程式變得更易讀。

在設計庫的時候你應記在腦袋裡的目標,並不是僅僅讓客戶代碼簡潔,而是讓它變得更可讀,更易懂。簡潔性經常是可讀性的重要部分,但不能簡潔的過了頭。通過設計出有助於簡潔,可讀,易懂的客戶代碼的庫,你將協助客戶程式員更多產地工作。

[1] require方法定義在scala包裡的孤立對象Predef上。4.4節中提到過,Predef對象的成員都已被自動引入到每個Scala源檔案中。

[2] 實際上,在that指的是調用add的對象時, Rational可以加到自己身上。但是因為你可以傳遞任何Rational對象給add,所以編譯器仍然不會讓你說that.n。

[3] 10.6節中你會發現參數化域:parametric field,提供了編寫同樣代碼的捷徑。

[4] 儘管n和d是用在類的函數體內,因為他們只是用在構造器之內,Scala編譯器將不會為它們自動構造域。所以就這些代碼來說,Scala編譯器將產生一個有兩個Int域的類,一個是numer,另一個是denom。

[5] 這種風格被稱為駝峰式:camel case,因為標識符由一個個首字母大寫的內嵌單片語成。

[6] 在16.5節裡,你將看到有些時候你需要給一種被稱為用例類:case class的特殊的類一個僅用操作符字元組成的名字。例如,Scala的API包含了一種叫做::的類,用來方便List裡的模式比對。

[7] 更精確地說,操作符字元屬於數學符號(Sm)或其他符號(So)的Unicode集,或不是字母,數字,括弧,方括弧,大括弧,單或雙引號,或底線,句號,分號,冒號,回退字元的7位ASCII字元。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.