Programming In Scala筆記-第七章、Scala中的控制結構

  所謂的內建控制結構是指編程語言中可以使用的一些代碼控制語法,如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)

  運行結果如下,即使某一次無任何輸入,該循環仍然會執行並繼續。
  這裏寫圖片描述
  
  對於純函數式編程語言來說,其中是沒有whiledo-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關鍵字。而switchmatch最大的不同是,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
  這裏寫圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章