Scala學習(四)內建控制器

來源:互聯網
上載者:User
第1章內建控制結構

Scala裡沒有多少內建控制結構。僅有的包括if,while,for,try,match和函數調用。如此之少的理由是,從一開始Scala就包括了函數文本。代之以在基本文法之上一個接一個添加高層級控制結構,Scala把它們彙集在庫裡。第9章將更細緻地展現如何做到這點。本章將展現僅有的幾個內建控制結構。

有件你會注意到的事情是,幾乎所有的Scala的控制結構都會產生某個值。這是函數式語言所採用的方式,程式被看成是計算值的活動,因此程式的控制項也應當這麼做。你也可以把這種方式看做早已存在於指令式語言中的一種趨勢(函數調用傳回值,被調用函數更新被當作參數傳入的輸出變數也歸於此類)的邏輯推演。另外,指令式語言經常具有三元操作符(如C,C++和Java的?:操作符),表現得就像if,卻產生值。Scala採用了這種三元操作符模型,但是把它稱為if。換句話說,Scala的if可以產生值。於是Scala持續了這種趨勢讓for,try和match也產生值。

程式員能夠利用這些結果值簡化他們的代碼,就好象用函數的傳回值那樣。如果沒有這種機制,程式員就必須建立臨時變數來儲存控制結構中計算的結果。去掉這些臨時變數能讓代碼更簡潔並避免許多你在一個分支裡設定了變數卻在另外一個分支裡忘了設定的bug。

總而言之,Scala的基礎控制結構雖然少但也足夠提供指令式語言裡重要的東西了。進一步說,由雩都具有結果值,它們能協助你縮短代碼。為了讓你看到所有這些都是怎麼工作的,本章將近距離展現Scala的基礎控制結構。

1.1 if運算式

Scala的if如同許多其它語言中的一樣工作。它測試一個狀態並據其是否為真,執行兩個分支中的一個。下面是一個常見的例子,以指令式風格編寫:

var filename = "default.txt"

if (!args.isEmpty)

filename = args(0)

這段代碼聲明了一個變數,filename,並初始化為預設值。然後使用if運算式檢查是否提供給程式了任何參數。如果是,就把變數改成定義在參數列表中的值。如果沒有參數,就任由變數設定為預設值。

這段代碼可以寫得更好一點,因為就像第2章第三步提到過的,Scala的if是能傳回值的運算式。代碼7.1展示了如何不使用任何var而實現前面一個例子同樣的效果:

val filename =

if (!args.isEmpty) args(0)

else "default.txt"

代碼 7.1 在Scala雷根據條件做初始化的慣例

這一次,if有了兩個分支。如果args不為空白,那麼初始化元素,args(0),被選中。否則,預設值被選中。這個if運算式產生了被選中的值,然後filename變數被初始化為這個值。這段代碼更短一點兒,不過它的實際優點在於使用了val而不是var。使用val是函數式的風格,並能以差不多與Java的final變數同樣的方式幫到你。它讓代碼的讀者確信這個變數將永不改變,節省了他們掃描變數欄位的所有代碼以檢查它是否改變的工作。

使用val而不是var的第二點好處是他能更好地支援等效推論:equational reasoning。在運算式沒有副作用的前提下,引入的變數等效於計算它的運算式。因此,無論何時都可以用運算式替代變數名。如,要替代println(filename),你可以這麼寫:

println(if (!args.isEmpty) args(0) else "default.txt")

選擇權在你。怎麼寫都行。使用val可以幫你安全地執行這類重構以不斷革新你的代碼。

儘可能尋找使用val的機會。它們能讓你的代碼既容易閱讀又容易重構。

1.2 while迴圈

Scala的while迴圈表現的和在其它語言中一樣。包括一個狀態和迴圈體,只要狀態為真,迴圈體就一遍遍被執行。代碼7.2展示了一個例子:

def gcdLoop(x: Long, y: Long): Long = {

var a = x

var b = y

while (a != 0) {

val temp = a

a = b % a

b = temp

}

b

}

代碼 7.2 用while迴圈計算最大公約數

Scala也有do-while迴圈。除了把狀態測試從前面移到後面之外,與while迴圈沒有區別。代碼7.3展示了使用do-while反饋從標準輸入讀入的行記錄直到讀入空行為止的Scala指令碼:

var line = ""

do {

line = readLine()

println("Read: " + line)

} while (line != null)

代碼 7.3 用do-while從標準輸入讀取資訊

while和do-while結構被稱為“迴圈”,不是運算式,因為它們不產生有意義的結果,結果的類型是Unit。說明產生的值(並且實際上是唯一的值)的類型為Unit。被稱為unit value,寫做()。()的存在是Scala的Unit不同於Java的void的地方。請在解譯器裡嘗試下列代碼:

scala> def greet() { println("hi") }

greet: ()Unit

scala> greet() == ()

hi

res0: Boolean = true

由於方法體之前沒有等號,greet被定義為結果類型為Unit的過程。因此,greet返回unit值,()。這被下一行確證:比較greet的結果和unit值,(),的相等性,產生true。

另一個產生unit值的與此相關的架構,是對var的再賦值。比如,假設嘗試用下面的從Java(或者C或C++)裡的while迴圈成例在Scala裡讀取一行記錄,你就遇到麻煩了:

var line = ""

while ((line = readLine()) != "") // 不起作用

println("Read: "+ line)

編譯這段代碼時,Scala會警告你使用!=比較類型為Unit和String的值將永遠產生true。而在Java裡,指派陳述式可以返回被賦予的那個值,同樣情況下標準輸入返回的一條記錄在Scala的指派陳述式中永遠產生unit值,()。因此,指派陳述式“line = readLine()”的值將永遠是()而不是""。結果,這個while迴圈的狀態將永遠不會是假,於是迴圈將因此永遠不會結束。

由於while迴圈不產生值,它它經常被純函數式語言所捨棄。這種語言只有運算式,沒有迴圈。雖然如此,Scala仍然包含了while迴圈,因為有些時候指令式的解決方案更可讀,尤其是對那些以指令式背景為主導的程式員來說。例如,如果你想做一段重複某進程直到某些狀態改變的演算法代碼,while迴圈可以直接地表達而函數式的替代者,大概要用遞迴實現,或許對某些代碼的讀者來說就不是那麼顯而易見的了。

如,代碼7.4展示了計算兩個數的最大公約數的替代方式。[1]給定同樣的值x和y,代碼7.4展示的gcd函數將返回與代碼7.2中gcdLoop函數同樣的結果。這兩種方式的不同在於gcdLoop寫成了指令式風格,使用了var和while迴圈,而gcd更函數式風格,採用了遞迴(gcd調用自身)並且不需要var:

def gcd(x: Long, y: Long): Long =

if (y == 0) x else gcd(y, x % y)

代碼 7.4 使用遞迴計算最大公約數

通常意義上,我們建議你如質疑var那樣質疑你代碼中的while迴圈。實際上,while迴圈和var經常是結對出現的。因為while迴圈不產生值,為了讓你的程式有任何改變,while迴圈通常不是更新var就是執行I/O。可以在之前的gcdLoop例子裡看到。在while迴圈工作的時候,更新了a和b兩個var。因此,我們建議你在代碼中對while迴圈抱有更懷疑的態度。如果沒有對特定的while或do迴圈較好的決斷,請嘗試找到不用它們也能做同樣事情的方式。

1.3 for運算式

Scala的for運算式是為枚舉準備的“瑞士軍刀”。它可以讓你用不同的方式把若干簡單的成分組合來表達各種各樣的枚舉。簡單的用法完成如把整數序列枚舉一遍那樣通常的任務。更進階的運算式可以列舉不同類型的多個集合,可以用任意條件過濾元素,還可以製造新的集合。

枚舉集合類

你能用for做的最簡單的事情就是把一個集合類的所有元素都枚舉一遍。如,代碼7.5展示了列印目前的目錄所有檔案名稱的例子。I/O操作使用了Java的API。首先,我們建立指向目前的目錄,".",的檔案,然後調用它的listFiles方法。方法返回File對象數組,每個都代表目前的目錄包含的目錄或檔案。我們把結果數組儲存在filesHere變數。

val filesHere = (new java.io.File(".")).listFiles

for (file <- filesHere)

println(file)

代碼 7.5 用for迴圈列表目錄中的檔案

通過使用被稱為發生器:generator的文法“file <- filesHere”,我們遍曆了filesHere的元素。每一次枚舉,名為file的新的val就被元素值初始化。編譯器推斷file的類型是File,因為filesHere是Array[File]。對於每一次枚舉,for運算式的函數體,println(file),將被執行一次。由於File的toString方法產生檔案或目錄的名稱,因此目前的目錄的所有檔案和目錄的名稱都會被列印出來。

for運算式文法對任何種類的集合類都有效,而不只是數組。[2]第80頁的表格5-4中看到的Range類型是其中一個方便的特例,你可以使用類似於“1 to 5”這樣的文法建立一個Range,然後用for枚舉。以下是一個簡單的例子:

scala> for (i <- 1 to 4)

println("Iteration " + i)

Iteration 1

Iteration 2

Iteration 3

Iteration 4

如果你不想包括被枚舉的Range的上邊界,可以用until替代to:

scala> for (i <- 1 until 4)

println("Iteration " + i)

Iteration 1

Iteration 2

Iteration 3

像這樣枚舉整數在Scala裡是很平常的,但在其他語言中就不是這麼回事。其它語言中,你或許要採用如下方式遍曆數組:

// Scala中不常見……

for (i <- 0 to filesHere.length - 1)

println(filesHere(i))

這個for運算式引入了變數i,依次把它設成從0到filesHere.length - 1的整數值,然後對i的每個設定執行一次for運算式的迴圈體。對應於每一個i的值,filesHere的第i個元素被取出並處理。

這種類型的枚舉在Scala裡不常見的原因是直接枚舉集合類也做得同樣好。這樣做,你的代碼變得更短並規避了許多枚舉數組時頻繁出現的超位溢出:off-by-one error。該從0開始還是從1開始?應該加-1,+1,還是什麼都不用直到最後一個索引?這些問題很容易回答,但也很容易答錯。還是避免碰到為佳。

過濾

有些時候你不想枚舉一個集合類的全部元素。而是想過濾出一個子集。你可以通過把過濾器:filter:一個if子句加到for的括弧裡做到。如代碼7.6的代碼僅對目前的目錄中以“.scala”結尾的檔案名稱做列表:

val filesHere = (new java.io.File(".")).listFiles

for (file <- filesHere if file.getName.endsWith(".scala"))

println(file)

代碼 7.6 用帶過濾器的for發現.scala檔案

或者你也可以這麼寫:

for (file <- filesHere)

if (file.getName.endsWith(".scala"))

println(file)

這段代碼可以產生與前一段代碼同樣的輸出,而且對於指令式背景的程式員來說看上去更熟悉一些。然而指令式格式只是一個可選項,因為這個for運算式的運用執行的目的是為了它的列印這個副作用併產生unit值()。正如在本節後面將展示的,for運算式之所以被稱為“運算式”是因為它能產生令人感興趣的值,一個其類型取決於for運算式<-子句的集合。

如果願意的話,你可以包含更多的過濾器。只要不斷加到子句裡即可。例如,為了加強防衛,代碼7.7中的代碼僅僅列印檔案而不是目錄。通過增加過濾器檢查file的isFile方法做到:

for (

file <- filesHere

if file.isFile;

if file.getName.endsWith(".scala")

) println(file)

代碼 7.7 在for運算式中使用多個過濾器

注意

如果在發生器中加入超過一個過濾器,if子句必須用分號分隔。這是代碼7.7中的“if file.isFile”過濾器之後帶著分號的原因。

嵌套枚舉

如果加入多個<-子句,你就得到了嵌套的“迴圈”。比如,代碼7.8展示的for運算式有兩個嵌套迴圈。外層的迴圈枚舉filesHere,內層的枚舉所有以.scala結尾檔案的fileLines(file)。

def fileLines(file: java.io.File) =

scala.io.Source.fromFile(file).getLines.toList

def grep(pattern: String) =

for {

file <- filesHere

if file.getName.endsWith(".scala")

line <- fileLines(file)

if line.trim.matches(pattern)

} println(file + ": " + line.trim)

grep(".*gcd.*")

代碼 7.8 在for運算式中使用多個發生器

如果願意的話,你可以使用大括弧代替小括弧環繞發生器和過濾器。使用大括弧的一個好處是你可以省略一些使用小括弧必須加的分號。

mid-stream(流間)變數綁定

請注意前面的程式碼片段中重複出現的運算式line.trim。這不是個可忽略的計算,因此你或許想每次只算一遍。通過用等號(=)把結果綁定到新變數可以做到這點。綁定的變數被當作val引入和使用,不過不用帶關鍵字val。代碼7.9展示了一個例子。

def grep(pattern: String) =

for {

file <- filesHere

if file.getName.endsWith(".scala")

line <- fileLines(file)

trimmed = line.trim

if trimmed.matches(pattern)

} println(file + ": " + trimmed)

grep(".*gcd.*")

代碼 7.9 在for運算式裡的流間賦值

代碼中,名為trimmed的變數被從半當中引入for運算式,並被初始化為line.trim的結果值。之後的for運算式就可以在兩個地方使用這個新變數,一次在if中,一次在println中。

製造新集合

到現在為止所有的例子都只是對枚舉值進行操作然後就放過,除此之外,你還可以建立一個值去記住每一次的迭代。只要在for運算式之前加上關鍵字yield。比如,下面的函數鑒別出.scala檔案並儲存在數組裡:

def scalaFiles =

for {

file <- filesHere

if file.getName.endsWith(".scala")

} yield file

for運算式在每次執行的時候都會製造一個值,本例中是file。當for運算式完成的時候,結果將是一個包含了所有產生的值的集合。結果集合的類型基於枚舉子句處理的集合類型。本例中結果為Array[File],因為filesHere是數組並且產生的運算式類型是File。

另外,請注意放置yield關鍵字的地方。對於for-yield運算式的文法是這樣的:

for {子句} yield {迴圈體}

yield在整個迴圈體之前。即使迴圈體是一個被大括弧包圍的代碼塊,也一定把yield放在左括弧之前,而不是代碼塊的最後一個運算式之前。請抵擋住寫成如下方式的誘惑:

for (file <-filesHere if file.getName.endsWith(".scala")) {

yield file // 語法錯誤!

}

例如,代碼7.10展示的for運算式首先把包含了所有目前的目錄的檔案的名為filesHere的Array[File],轉換成一個僅包含.scala檔案的數組。對於每一個對象,產生一個Iterator[String](fileLines方法的結果,定義展示在代碼7.8中),提供方法next和hasNext讓你枚舉集合的每個元素。這個原始的列舉程式又被轉換為另一個Iterator[String]僅包含含有子字串"for"的修剪過的行。最終,對每一行產生整數長度。這個for運算式的結果就是一個包含了這些長度的Array[Int]數組。

val forLineLengths =

for {

file <- filesHere

if file.getName.endsWith(".scala")

line <- fileLines(file)

trimmed = line.trim

if trimmed.matches(".*for.*")

} yield trimmed.length

代碼 7.10 用for把Array[File]轉換為Array[Int]

目前,你已經看過了Scala的for運算式所有主要的特徵。然而這個段落過得實在是快了一些。for運算式更透徹的介紹在第二十三章。

1.4 使用try運算式處理異常

Scala的異常和許多其它語言的一樣。代之用普通方式那樣返回一個值,方法可以通過拋出一個異常中止。方法的調用者要麼可以捕獲並處理這個異常,或者也可以簡單地中止掉,並把異常升級到調用者的調用者。異常可以就這麼升級,一層層釋放呼叫堆疊,直到某個方法處理了它或沒有剩下其它的方法。

拋出異常

異常的拋出看上去與Java的一模一樣。首先建立一個異常對象然後用throw關鍵字拋出:

throw new IllegalArgumentException

儘管可能感覺有些出乎意料,Scala裡, throw也是有結果類型的運算式。下面舉一個有關結果類型的例子:

val half =

if (n % 2 == 0)

n / 2

else

throw new RuntimeException("n must be even")

這裡發生的事情是,如果n是偶數,half將被初始化為n的一半。如果n不是偶數,那麼在half能被初始化為任何值之前異常將被拋出。因此,無論怎麼說,把拋出的異常當作任何類型的值都是安全的。任何使用從throw傳回值的嘗試都不會起作用,因此這樣做無害。

從技術角度上來說,拋出異常的類型是Nothing。儘管throw不實際得出任何值,你還是可以把它當作運算式。這種小技巧或許看上去很怪異,但像在上面這樣的例子裡卻常常很有用。if的一個分支計算值,另一個拋出異常並得出Nothing。整個if運算式的類型就是那個實際計算值的分支的類型。Nothing類型將在以後的11.3節中討論。

捕獲異常

用來捕獲異常的文法展示在代碼7.11中。選擇catch子句這樣的文法的原因是為了與Scala很重要的部分:模式比對:pattern matching保持一致。模式比對是一種很強大的特徵,將在本章概述並在第十五章詳述。

import java.io.FileReader

import java.io.FileNotFoundException

import java.io.IOException

try {

val f = new FileReader("input.txt")

// Use and close file

} catch {

case ex: FileNotFoundException => // Handle missing file

case ex: IOException => // Handle other I/O error

}

代碼 7.11 Scala的try-catch子句

這個try-catch運算式的行為與其它語言中的異常處理一致。程式體被執行,如果拋出異常,每個catch子句依次被嘗試。本例中,如果異常是FileNotFoundException,那麼第一個子句將被執行。如果是IOException類型,第二個子句將被執行。如果都不是,那麼try-catch將終結並把異常上升出去。

注意

你將很快發現與Java的一個差別是Scala裡不需要你捕獲檢查異常:checked exception,或把它們聲明在throws子句中。如果你願意,可以用ATthrows標註聲明一個throws子句,但這不是必需的。

finally子句

如果想讓某些代碼無論方法如何中止都要執行的話,可以把運算式放在finally子句裡。如,你或許想讓開啟的檔案即使是方法拋出異常退出也要確保被關閉。代碼7.12展示了這個例子。

import java.io.FileReader

val file = openFile()

try {

// 使用檔案

} finally {

file.close() // 確保關閉檔案

}

代碼 7.12 Scala的try-finally子句

注意

代碼7.12展示了確保非記憶體資源,如檔案,通訊端,或資料庫連結被關閉的慣例方式。首先你獲得了資源。然後你開始一個try代碼塊使用資源。最後,你在finally代碼塊中關閉資源。這種Scala裡的慣例與在Java裡的一樣,然而,Scala裡你還使用另一種被稱為貸出模式:loan pattern的技巧更簡潔地達到同樣的目的。出借模式將在9.4節描述。

產生值

和其它大多數Scala控制結構一樣,try-catch-finally也產生值。如,代碼7.13展示了如何嘗試拆分URL,但如果URL格式錯誤就使用預設值。結果是,如果沒有異常拋出,則對應於try子句;如果拋出異常並被捕獲,則對應於相應的catch子句。如果異常被拋出但沒被捕獲,運算式就沒有傳回值。由finally子句計算得到的值,如果有的話,被拋棄。通常finally子句做一些清理類型的工作如關閉檔案;他們不應該改變在主函數體或try的catch子句中計算的值。

import java.net.URL

import java.net.MalformedURLException

def urlFor(path: String) =

try {

new URL(path)

} catch {

case e: MalformedURLException =>

new URL("http://www.scalalang.org")

}

代碼 7.13 能夠產生值的catch子句

如果熟悉Java,不說你也知道,Scala的行為與Java的差別僅源於Java的try-finally不產生值。Java裡,如果finally子句包含一個顯式返回語句,或拋出一個異常,這個傳回值或異常將“淩駕”於任何之前源於try代碼塊或某個它的catch子句產生的值或異常之上。如:

def f(): Int = try { return 1 } finally { return 2 }

調用f()產生結果值2。相反:

def g(): Int = try { 1 } finally { 2 }

調用g()產生1。這兩個例子展示了有可能另大多數程式員感到驚奇的行為,因此通常最好還是避免從finally子句中傳回值。最好是把finally子句當作確保某些副作用,如關閉開啟的檔案,發生的途徑。

1.5 match運算式

Scala的匹配運算式允許你在許多可選項:alternative中做選擇,就好象其它語言中的switch語句。通常說來match運算式可以讓你使用任意的模式:pattern做選擇,第十五章會介紹。通用的模式可以稍等再說。目前,只要考慮使用match在若干可選項中做選擇。

作為例子,代碼7.14裡的指令碼從參數列表讀入食物名然後列印食物配料。match運算式檢查參數列表的第一個參數firstArg。如果是字串"salt",就列印"pepper",如果是"chips",就列印"salsa",如此遞推。預設情況用底線(_)說明,這是常用在Scala裡作為預留位置表示完全不清楚的值的萬用字元。

val firstArg = if (args.length > 0) args(0) else ""

firstArg match {

case "salt" => println("pepper")

case "chips" => println("salsa")

case "eggs" => println("bacon")

case _ => println("huh?")

}

代碼 7.14 有副作用的match運算式

與Java的switch語句比,匹配運算式還有一些重要的差別。其中之一是任何種類的常量,或其他什麼東西,都能用作Scala裡的case,而不只是Java的case語句裡面的整數類型和枚舉常量。在這個例子裡,可選項是字串。另一個區別是在每個可選項的最後並沒有break。取而代之,break是隱含的,不會有從一個可選項轉到另一個裡面去的情況。這通常把代碼變短了,並且避免了一些錯誤的根源,因為程式員不再因為疏忽在選項裡轉來轉去。

然而,與Java的switch相比最顯著的差別,或許是match運算式也能產生值。在前一個例子裡,match運算式的每個可選項列印輸出一個值。只產生值而不是列印也可以一樣做到,展示在代碼7.15中。match運算式產生的值儲存在friend變數裡。這除了能讓代碼變得更短之外(至少減少了幾個指令),還解開了兩個不相干的關注點:首先選擇食物名,其次列印它。

val firstArg = if (!args.isEmpty) args(0) else ""

val friend =

firstArg match {

case "salt" => "pepper"

case "chips" => "salsa"

case "eggs" => "bacon"

case _ => "huh?"

}

println(friend)

代碼 7.15 產生值的match運算式

1.6 離開break和continue

你可能注意到了這裡沒有提到過break和continue。Scala去掉了這些命令因為他們與函數式文本,下一章會談到這個特徵,齧合得不好。continue在while迴圈中的意思很清楚,但是在函數式文本中表示什麼呢?雖然Scala既支援指令式風格也支援函數式風格,但在這點上它略微傾向於函數式編程從而換得在語言上的簡潔性。儘管如此,請不要著急。有許多不用break和continue的編程方式,如果你能有效利用函數式文本,就能比原來的代碼寫得更短。

最簡單的方式是用if替換每個every和用布爾變數替換每個break。布爾變數指代是否包含它的while迴圈應該繼續。比如說,假設你正搜尋一個參數列表去尋找以“.scala”結尾但不以連號開頭的字串。Java裡你可以——如果你很喜歡while迴圈,break和continue——如此寫:

int i = 0; // 在Java中……

boolean foundIt = false;

while (i < args.length) {

if (args[i].startsWith("-"))

{

i = i + 1;

continue;

}

if (args[i].endsWith(".scala")) {

foundIt = true;

break;

}

i = i + 1;

}

如果要字面直譯成Scala的代碼,代之以執行一個if然後continue,你可以寫一個if環繞while餘下的全部內容。要去掉break,你可以增加一個布爾變數提示是否繼續做下去,不過在這裡你可以複用foundIt。使用這兩個技巧,代碼就可以像代碼7.16這樣完成了:

var i = 0

var foundIt = false

while (i < args.length && !foundIt) {

if (!args(i).startsWith(""))

{

if (args(i).endsWith(".scala"))

foundIt = true

}

i = i + 1

}

代碼 7.16 不帶break或continue的迴圈

這個版本與原來的Java代碼非常像。所有的主要段落仍然存在並保持原順序。有兩個可重新賦值的變數及一個while迴圈。迴圈內有個i是否小於args.length的測試,然後檢查"-",然後檢查".scala"。

如果要去掉代碼7.16裡面的var,你可以嘗試的一種方式是用遞迴函式重寫迴圈。比方說,你可以定義帶一個整數值做輸入的searchFrom函數,向前搜尋,並返回想要的參數的索引。採用這種技巧的代碼看上去會像展示在代碼7.17中這樣的:

def searchFrom(i: Int): Int =

if (i >= args.length) -1// 不要越過最後一個參數

else if (args(i).startsWith("-")) searchFrom(i + 1)// 跳過選項

else if (args(i).endsWith(".scala")) i // 找到!

else searchFrom(i + 1) // 繼續找

val i = searchFrom(0)

代碼 7.17 不用var做迴圈的遞迴替代方法

代碼7.17的版本提供了一個能夠看得懂的名字說明這個函數在做什麼,它用遞迴替代了迴圈。每個continue都被帶有i + 1做參數的遞迴調用替換掉,有效地跳轉到下一個整數。許多人都發現當他們開始使用遞迴後,這種編程風格更易於理解。

注意

Scala編譯器不會實際對代碼7.17展示的代碼產生遞迴函式。因為所有的遞迴調用都在尾調用:tail-call位置,編譯器會產生出與while迴圈類似的代碼。每個遞迴調用將被實現為回到函數開始位置的跳轉。尾調用最佳化將在8.9節討論。

1.7 變數範圍

現在你已經看過了Scala的內建控制結構,我們將在本節中使用它們來解釋Scala裡的範圍是如何起作用的。

Java程式員的快速通道

如果你是Java程式員,你會發現Scala的範圍規則幾乎是Java的翻版。然而,兩者之間仍然有一個差別,Scala允許你在嵌套範圍內定義同名變數。因此如果你是Java程式員,或許至少還是快速探索一下。

Scala程式裡的變數定義有一個能夠使用的範圍:scope。範圍設定的最普通不過的例子就是,大括弧通常引入了一個新的範圍,所以任何定義在打括弧裡的東西在括弧之後就脫離了範圍。[3]作為示範,請看一下代碼7.18裡展示的函數:

def printMultiTable() {

var i = 1

// 這裡只有i在範圍內

while (i <= 10) {

var j = 1

// 這裡i和j在範圍內

while (j <= 10) {

val prod = (i * j).toString

// 這裡i,j和prod在範圍內

var k = prod.length

// 這裡i,j,prod和k在範圍內

while (k < 4) {

print(" ")

k += 1

}

print(prod)

j += 1

}

// i和j仍在範圍內;prod和k脫離範圍

println()

i += 1

}

// i仍在範圍內;j,prod和k脫離範圍

}

代碼 7.18 列印乘法表時的變數範圍

printMultiTable函數列印了乘法表。[4]函數的第一個語句引入了變數i並初始化為整數1。然後你可以在函數餘下的部分裡使用名稱i。

printMultiTable接下去的語句是一個while迴圈:

while (i <= 10) {

var j = 1

...

}

你可以在這使用i因為它仍在範圍內。在while迴圈的第一個語句裡,你引入了另一個變數,叫做j,並再次初始化為1。因為變數j定義在while迴圈的大括弧內,所以只能用在while迴圈裡。如果你想嘗試在while迴圈的大括弧之後,在那個說j,prod和k已經出了範圍的注釋後面,再用j做點兒什麼事,你的程式就編譯不過了。

本例中定義的所有變數——i,j,prod和k——都是本地變數:local variable。對於它們被定義的函數來說是“本地”的。每次函數被調用的時候,一整套全新的本地變數將被使用。

一旦變數被定義了,你就不可以在同一個範圍內定義同樣的名字。比如,下面的指令碼不會被編譯通過:

val a = 1

val a = 2 // 編譯不過

println(a)

然而,你可以在一個內部範圍內定義與外部範圍裡名稱相同的變數。下列指令碼將編譯通過並可以運行:

val a = 1;

{

val a = 2 // 編譯通過

println(a)

}

println(a)

執行時,這個指令碼會先列印2,然後列印1,因為定義在內部打括弧裡的a是不同的變數,將僅在大括弧內部有效。[5]Scala和Java間要注意的一個不同是,與Scala不同,Java不允許你在內部範圍內建立與外部範圍變數同名的變數。在Scala程式裡,內部變數被說成是遮蔽:shadow了同名的外部變數,因為在內部範圍內外部變數變得不可見了。

或許你已經注意到了一些在解譯器裡看上去像是遮蔽的東西:

scala> val a = 1

a: Int = 1

scala> val a = 2

a: Int = 2

scala> println(a)

2

解譯器裡,你可以對你的核心內容重用變數名。撇開別的不說,這樣能允許你當發現你在解譯器裡第一次定義變數時犯了錯誤的時候改變主意。你能這麼做的理由是因為,在理論上,解譯器在每次你輸入新的語句時都建立了一個新的嵌套範圍。因此,你可以把之前解釋的代碼虛擬化認為是:

val a = 1;

{

var a = 2;

{

println(a)

}

}

這段代碼可以像Scala指令碼那樣編譯和執行,而且像輸入到解譯器裡的代碼那樣,列印輸出2。請記住這樣的代碼對讀者來說是很混亂的,因為在嵌套範圍中變數名稱擁有了新的涵義。通常更好的辦法是選擇一個新的有意義的變數名而不是遮蔽外部變數。

1.8 重構指令式風格的代碼

為了協助你在函數式風格上獲得更多的領悟,本節我們將重構代碼7.18中以指令式風格列印乘法表的方式。我們的函數式替代品展示在代碼7.19中。

代碼7.18中的代碼在兩個方面顯示出了指令式風格。首先,調用printMultiTable有副作用:在標準輸出上列印乘法表。代碼7.19中,我們重構了函數,讓它把乘法表作為字串返回。由於函數不再執行列印,我們把它重新命名為multiTable。正如前面提到過的,沒有副作用的函數的一個優點是它們很容易進行單元測試。要測試printMultiTable,你需要重定義print和println從而能夠檢查輸出的正確性。測試multiTable就簡單多了,只要檢查結果即可。

// 以序列形式返回一行乘法表

def makeRowSeq(row: Int) =

for (col <- 1 to 10) yield {

val prod = (row * col).toString

val padding = " " * (4 - prod.length)

padding + prod

}

// 以字串形式返回一行乘法表

def makeRow(row: Int) = makeRowSeq(row).mkString

// 以字串形式返回乘法表,每行記錄佔一行字串

def multiTable() = {

val tableSeq = // 行記錄字串的序列

for (row <- 1 to 10)

yield makeRow(row)

tableSeq.mkString("\n")

}

代碼 7.19 建立乘法表的函數式方法

printMultiTable裡另一個揭露其指令式風格的訊號來自於它的while迴圈和var。與之相對,multiTable函數使用了val,for運算式,協助函數:helper function,並調用了mkString。

我們提煉出兩個協助函數,makeRow和makeRowSeq,使代碼容易閱讀。函數makeRowSeq使用for運算式從1到10枚舉列數。這個for函數體計算行和列的乘積,決定乘積前佔位的空格,並產生由佔位空格,乘積字串疊加成的結果。for運算式的結果是一個包含了這些產生字串作為元素的序列(scala.Seq的某個子類)。另一個協助函數,makeRow,僅僅調用了makeRowSeq返回結果的mkString函數。疊加序列中的字串把它們作為一個字串返回。

multiTable方法首先使用一個for運算式的結果初始化tableSeq,這個for運算式從1到10枚舉行數,對每行調用makeRow獲得該行的字串。因為字串首碼yield關鍵字,所以運算式的結果就是行字串的序列。現在僅剩下的工作就是把字串序列轉變為單一字串。mkString的調用完成這個工作,並且由於我們傳遞進去"\n",因此每個字串結尾插入了分行符號。如果把multiTable返回的字串傳遞給println,你將看到與調用printMultiTable所產生的同樣的輸出結果:(略)

[1] 代碼 七-4中展示的gcd函數使用了首先在代碼 六-3中展示的同名函數,為類Rational計算最大公約數,所使用的同樣的方法,主要的差別在於代碼 七-4的gcd的參數使用Long而不是Int。

[2] 更精確地說,在<-符號右側的運算式必須支援名為foreach的方法。

[3] 這條規則有幾個例外,因為在Scala裡有時候你可以用大括弧代替小括弧。運算式文法的替代品是這種使用大括弧例子的其中之一,將在7.3節描述。

[4] 代碼 七-18展示的printMultiTable函數是用指令式風格寫的。我們將在下一節中以函數式風格重構。

[5] 另外,本例中在a的第一個定義之後需要加分號,因為Scala的分號推斷機制不會在這裡加上分號。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.