所謂的內建控制結構是指編程語言中可以使用的一些代碼控制語法,如Scala中的if, while, for, try, match, 以及函數調用等。需要注意的是,Scala幾乎所有的內建控制結構都會返回一個值,這是由於函數式編程語言被認爲是計算值的過程,所以作爲函數式編程語言的一個組件,這些內建控制結構也不例外。
如果不好理解函數式編程語言中每一個內建控制結構都會返回一個值這一概念,可以回想一下? :
表達式,這個表達式基本上能表明這一概念,作用和if
表達式類似,但是會根據條件得到一個分支的值作爲返回值。
一、if表達式
Scala的if語句和其他語言中的類似,傳入一個判斷條件。
比如,在指令式語言中,我們常常這樣寫
var filename = "default.txt"
if (!args.isEnpty)
filename = args(0)
上面的代碼中,首先定義一個變量filename並賦給一個初始化值。如果滿足if中的判斷條件,則將該值進行更新。
在Scala中,可以對上面的代碼進行簡化,不在需要定義一個變量並且根據條件更新其值。
val filename =
if (!args.isEmpty) args(0)
else "default.txt"
if表達式會返回一個值,根據判斷條件決定該返回值及類型取自if表達式的哪一個分支。並且當該表達式沒有副作用時,直接使用該表達式得到的值和定義一個val變量並賦值的作用是相同的。所以,如果只需要將該文件名打印出來,可以進一步簡化成
println(if (!args.isEmpty) args(0) else "default.txt")
不過,一般建議還是先將該值賦給一個變量,然後輸出該變量的值。這樣能夠保證代碼的可讀性和重構性。
二、while循環
1、while循環
while循環有一個判斷條件和一個循環體,只要滿足判斷條件,該循環體就會被執行。下面展示一段求最大公約數的while循環代碼
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
}
2、do-while循環
同樣,Scala也提供一個do-while循環結構,和上面這個不同的是,do-while循環首先執行循環體,然後判斷循環條件是否滿足,以決定下一次循環是否繼續。下面這段代碼,循環讀取輸入文本,直到遇到空行爲止。
val line = ""
do {
line = readLine()
print("Read: " + line)
} while (line != "")
3、Unit返回值
while和do-while都被稱爲循環,而不是表達式。這時由於while和do-while循環不會得到一個返回值,或者是它們的返回值爲Unit。Unit值被寫成(),如下所示,定義一個返回值爲Unit的函數greet
def greet() {println("hi")}
greet() == ()
結果如下,表示greet函數的返回值與()是相等的。
需要注意的是,在Scala中對一個var變量重新賦值的語句得到的返回值也是Unit。
比如,如果執行下面這段代碼,Scala編譯器會認爲將一個Unit類型的值與""
對比,結果永遠爲true
var line = ""
while ((line = readLine()) != "")
println("Read: " + line)
運行結果如下,即使某一次無任何輸入,該循環仍然會執行並繼續。
對於純函數式編程語言來說,其中是沒有while
和do-while
循環這一概念的。但是Scala提供了這一功能,while循環的使用,使得代碼可讀性更強。而如果不使用while循環的話,有些代碼會寫出遞歸調用的形式,比如下面採用遞歸的方法計算最大公約數。
def gcd(x: Long, y: Long): Long =
if (y == 0) x else gcd(y, x % y)
三、for表達式
for表達可以用於簡單的字面變量以及集合類型變量,也可以在遍歷時使用一些過濾條件進行過濾,還能夠基於舊的集合生成新的集合對象。
1、枚舉集合類型
下面這段代碼,獲取當前路徑下所有的文件,並打印出來。其中filesHere變量是一個數組類型的變量,使用file <- filesHere
循環遍歷數組中的每一個元素。
val filesHere = (new java.io.File(".")).listFiles
for (file <- filesHere)
println(file)
運行結果如下所示,
上面展示的是數組類型變量的for表達式,同樣的for表達式還可以用於更多其他類型的變量遍歷上。比如
for (i <- 1 to 4)
println("Iteration " + i)
結果如下
2、增加過濾條件
也可以在for表達式中增加一個過濾條件,篩選出其中滿足條件的元素進行處理。比如下面這段代碼,也是列舉出當前路徑下的所有文件,但是隻顯示其中.scala類型的文件。
val filesHere = (new java.io.File(".")).listFiles
for (file <- filesHere if file.getName.endsWith(".scala"))
println(file)
結果如下:
在循環判斷中,也可以增加多個判斷條件。
for (
file <- filesHere
if file.isFile
if file.getName.endsWith(".scala")
) println(file)
3、嵌套循環
如果使用多個<-
表達式,就會得到一個嵌套循環結構。例如下面代碼中有一個兩層嵌套。外層循環遍歷filesHere
遍歷,內層循環遍歷fileLines
方法讀取到的文件中每一行的內容。
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.*")
注意這裏將for
表達式的圓括號換成了花括號。
4、流間變量綁定
上面那段嵌套循環的代碼中,重複執行了line.trim代碼。如果不想重複的話,可以將line.trim
的結果通過=符合賦值給一個val變量,這個變量的val關鍵字可以省略不寫。
在下面這段代碼中,trimmed
變量在for表達式中被引入,並且初始化爲line.trim
。接下來的代碼中兩次使用到了這個trimmed
變量,一次是在if表達式中,一次是在println
操作中。
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.*")
在原文中,這一小節的英語表述是(Mid-stream variable bindings)。 這裏的流間變量綁定是指在for
表達式中引入的變量,可以在for
表達式的後續其他代碼中使用該變量。
5、生成新的集合變量
之所以這裏稱for
爲表達式,而不是for
循環,是因爲for
表達式可以獲得一個返回值。需要使用到關鍵字yield
。
(1)生成源集合相同類型的新集合
例如下面的代碼,找出當前路徑下所有的.scala類型的文件,並賦值給一個function。該for
表達式函數體每一次執行,都會產生一個file值。當filesHere
遍歷完成之後,會將所有yield出來的file值存儲到同一個集合類型對象中。返回集合的元素類型,與源集合中的元素類型相同。
def scalaFiles =
for {
file <- filesHere
if file.getName.endsWith(".scala")
} yield file
結果如下,
上面這段代碼中需要注意的是yield關鍵字的位置。正確的格式應該是下面這樣的,
for clauses yield body
由於上面代碼中的函數體比較簡單,看不出有什麼異常,如果按照下面這種寫法,就是錯誤的
for ( file <- filesHere if file.getName.endsWith(".scala") {
yield file
}
這種錯誤寫法中,yield
關鍵字就是寫入了函數體中。
(2)生成其他類型的集合
其實在Scala的for表達式中,也可以生成與源集合不同類型的集合。比如下面這段代碼中,首先獲取當前路徑下的所有.scala文件,然後對每一個文件讀取其中的所有行,去重,然後取出其中只包含for
關鍵字的代碼,最後,獲取這幾行代碼的字符個數。
其中的fileLines
函數得到一個Iterator[String]
類型的返回值。
val forLineLengths =
for {
file <- filesHere
if file.getName.endsWith(".scala")
line <- fileLines(file)
trimmed = line.trim
if trimmed.matches(".*for.*")
} yield trimmed.length
總結一下,有關於for
表達式,可以很方便的遍歷某個集合,也可在遍歷時加入過濾條件對循環的集合進行篩選。同時也可以在for
表達式中多次寫入<-
實現嵌套循環,並且,for
表達式中引入的變量可以省略val
關鍵字並在後續多次使用。最後,面對複雜的for
表達式時,最好不用圓括號,而改爲使用花括號。
四、try表達式異常處理
Scala中的表達式,除了正常執行並得到一個返回值的情況外,還有一種是運行時出現異常。出現異常時,程序可能會被終止,也可以由方法調用者捕獲並對該異常作出處理。
1、拋出異常
Scala拋出異常和Java類似,同樣適用throw
關鍵字,拋出的這個異常類,也可以由開發者自定義。
throw new IllegalArgumentException
對於throw
表達式,Scala其實也對應了一個返回值,其類型爲Nothing。
比如下面求一個偶數的一半,當傳入的n
是偶數時才進行計算,否則直接拋出一個RuntimeException。
val half = (
if (n % 2 == 0)
n / 2
else
throw new RuntimeException("n must be even"))
運行結果如下,當n爲3時,直接拋出一個異常,而當n爲4時,返回一個Int值2。
前面提到,if
表達式返回值的類型爲兩個分支計算值的公共父類,而現在throw
表達式的的返回值爲Nothing
,並且在Scala的類層級關係中Nothing
是任何類的直接或間接子類。也就是說,對於if
表達式來說,某一個分支是throw
表達式的話,那麼該if
表達式最終返回值的類型是有值計算的那一個分支的類型。
2、捕獲異常
拋出了異常,如果不加處理,程序就會終止。拋出異常後,可以使用catch
關鍵字捕獲該異常,並針對不同的異常作出不同的處理方案。下面的代碼展示了一個完成的try...catch
代碼結構
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
}
當拋出異常時,會順序執行catch
代碼塊中的邏輯。上面代碼中,如果拋出FileNotFoundException
,那麼就會執行文件不存在這一分支的代碼。而如果拋出IOException
異常,就會執行I/O異常這一部分的邏輯。而如果這兩種異常都匹配不到拋出的異常時,程序還是會終止。
3、finally子句
類似於Java代碼中,有時候遇到拋出異常而導致程序終止時,可能還希望執行一些收尾的邏輯。比如下面這段代碼中,如果讀取文件時拋出異常導致程序終止,那麼最好在終止前將打開的文件關閉。關閉文件的這段邏輯就寫在finally
子句中。
import java.io.FileReader
val file = new FileReader("input.txt")
try {
// Use the file
} finally {
file.close() // Be sure to close the file
}
4、產生一個返回值
類似於其他的控制結構,try-catch-finally
代碼也會產生一個返回值。
比如下面代碼中,對一個URL地址進行處理,如果不拋出異常,那麼使用傳入的地址構造一個URL對象。如果拋出異常並被捕獲到的話,則使用默認地址構造一個URL對象。
import java.net.URL
import java.net.MalformedURLException
def urlFor(path: String) =
try {
new URL(path)
} catch {
case e: MalformedURLException =>
new URL("http://www.scala-lang.org")
}
如果拋出異常並沒有被捕獲到,那麼返回的結果爲Nothing
。而finally
子句中設置的返回值是無法獲取到的,這也是由於程序終止時一般在finally
中只需要做一些收尾的操作,而不需要去改變某些變量的值。
如果在finally
中顯示的return
了某個返回值,或者拋出一個異常,那麼這個返回值或者拋出的異常會將之前try-catch
中應有的返回值覆蓋掉。比如
def f(): Int = try {return 1} finally {return 2}
結果如下:
而不顯示返回一個值時,最終的返回值爲try中的內容。比如
def g(): Int = try {1} finally {2}
結果如下:
五、match表達式
match
表達式類似於switch
表達式,可以在多個待選值中進行選擇。
看下面的代碼,如果args
的長度大於0,那麼firstArg
變量的值爲第一個元素,否則爲空字符串。然後,根據firstArg
變量的具體內容,如果是salt
則打印paper
,如果是eggs
則打印bacon
。而如果不是這三種中的任何一個,則打印huh?
。下面代碼中的下劃線_
表示任意匹配。
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?")
}
和Java中的switch
不同的是,Scala中的match
語法可以匹配包括整數,字符串等任意類型的變量,並且在每個case
後沒有break
關鍵字。而switch
和match
最大的不同是,match
表達式也會有一個返回值。上面的代碼片段中,打印每一種情況的顯示內容,而接下來這一段代碼,會根據匹配情況將字符串賦值給一個變量。
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)
可以從下面的結果看到,friend
的值爲salsa
。