Scala 3 — 書籍

控制結構

語言

Scala 具有您預期在程式語言中找到的控制結構,包括

  • if/then/else
  • for 迴圈
  • while 迴圈
  • try/catch/finally

它還有另外兩個強大的建構,您可能以前沒看過,這取決於您的程式設計背景

  • for 表達式(也稱為 for 理解
  • match 表達式

以下各節都示範了這些內容。

if/then/else 建構

單行 Scala if 陳述式如下所示

if (x == 1) println(x)
if x == 1 then println(x)

當您需要在 if 等號比較後執行多行程式碼時,請使用此語法

if (x == 1) {
  println("x is 1, as you can see:")
  println(x)
}
if x == 1 then
  println("x is 1, as you can see:")
  println(x)

if/else 語法如下所示

if (x == 1) {
  println("x is 1, as you can see:")
  println(x)
} else {
  println("x was not 1")
}
if x == 1 then
  println("x is 1, as you can see:")
  println(x)
else
  println("x was not 1")

而這是 if/else if/else 語法

if (x < 0)
  println("negative")
else if (x == 0)
  println("zero")
else
  println("positive")
if x < 0 then
  println("negative")
else if x == 0 then
  println("zero")
else
  println("positive")

end if 陳述式

  這是 Scala 3 的新功能,Scala 2 不支援。

如果你喜歡,可以選擇在每個表達式的結尾包含一個 end if 陳述式

if x == 1 then
  println("x is 1, as you can see:")
  println(x)
end if

if/else 表達式總是會傳回一個結果

請注意 if/else 比較會形成表達式,表示它們會傳回一個值,你可以將其指定給變數。因此,不需要一個特殊的條件運算子

val minValue = if (a < b) a else b
val minValue = if a < b then a else b

由於它們會傳回一個值,你可以使用 if/else 表達式作為方法的主體

def compare(a: Int, b: Int): Int =
  if (a < b)
    -1
  else if (a == b)
    0
  else
    1
def compare(a: Int, b: Int): Int =
  if a < b then
    -1
  else if a == b then
    0
  else
    1

補充:面向表達式的程式設計

關於一般程式設計的簡短說明,當你寫的每個表達式都會傳回一個值時,這種風格稱為面向表達式的程式設計或 EOP。例如,這是一個表達式

val minValue = if (a < b) a else b
val minValue = if a < b then a else b

相反地,不會傳回值的程式碼行稱為陳述式,它們用於其副作用。例如,這些程式碼行不會傳回值,因此它們用於其副作用

if (a == b) action()
println("Hello")
if a == b then action()
println("Hello")

第一個範例會在 a 等於 b 時,以副作用的方式執行 action 方法。第二個範例用於將字串印出到 STDOUT 的副作用。隨著你對 Scala 了解得更多,你會發現自己寫的表達式越來越多,陳述式越來越少。

for 迴圈

在最簡單的使用方式中,Scala for 迴圈可以用來反覆運算集合中的元素。例如,給定一個整數序列,你可以反覆運算其元素並印出它們的值,如下所示

val ints = Seq(1, 2, 3)
for (i <- ints) println(i)
val ints = Seq(1, 2, 3)
for i <- ints do println(i)

程式碼 i <- ints 稱為產生器。在任何產生器 p <- e 中,表達式 e 可以產生零個或多個與樣式 p 的繫結。

以下是在 Scala REPL 中的結果

scala> val ints = Seq(1,2,3)
ints: Seq[Int] = List(1, 2, 3)

scala> for (i <- ints) println(i)
1
2
3
scala> val ints = Seq(1,2,3)
ints: Seq[Int] = List(1, 2, 3)

scala> for i <- ints do println(i)
1
2
3

當你需要在 for 產生器後面加上多行程式碼區塊時,請使用以下語法

for (i <- ints) {
  val x = i * 2
  println(s"i = $i, x = $x")
}
for i <- ints
do
  val x = i * 2
  println(s"i = $i, x = $x")

多個產生器

for 迴圈可以有多個產生器,此範例說明

for {
  i <- 1 to 2
  j <- 'a' to 'b'
  k <- 1 to 10 by 5
} {
  println(s"i = $i, j = $j, k = $k")
}
for
  i <- 1 to 2
  j <- 'a' to 'b'
  k <- 1 to 10 by 5
do
  println(s"i = $i, j = $j, k = $k")

此表達式列印此輸出

i = 1, j = a, k = 1
i = 1, j = a, k = 6
i = 1, j = b, k = 1
i = 1, j = b, k = 6
i = 2, j = a, k = 1
i = 2, j = a, k = 6
i = 2, j = b, k = 1
i = 2, j = b, k = 6

守衛

for 迴圈也可以包含 if 陳述式,稱為守衛

for {
  i <- 1 to 5
  if i % 2 == 0
} {
  println(i)
}
for
  i <- 1 to 5
  if i % 2 == 0
do
  println(i)

此迴圈的輸出為

2
4

for 迴圈可以有任意數量的守衛。此範例顯示列印數字 4 的方式之一

for {
  i <- 1 to 10
  if i > 3
  if i < 6
  if i % 2 == 0
} {
  println(i)
}
for
  i <- 1 to 10
  if i > 3
  if i < 6
  if i % 2 == 0
do
  println(i)

使用 for 與 Map

您也可以將 for 迴圈與 Map 搭配使用。例如,給定此 Map,其中包含州縮寫及其全名

val states = Map(
  "AK" -> "Alaska",
  "AL" -> "Alabama", 
  "AR" -> "Arizona"
)

您可以使用 for 列印金鑰和值,如下所示

for ((abbrev, fullName) <- states) println(s"$abbrev: $fullName")
for (abbrev, fullName) <- states do println(s"$abbrev: $fullName")

以下是 REPL 中的顯示方式

scala> for ((abbrev, fullName) <- states) println(s"$abbrev: $fullName")
AK: Alaska
AL: Alabama
AR: Arizona
scala> for (abbrev, fullName) <- states do println(s"$abbrev: $fullName")
AK: Alaska
AL: Alabama
AR: Arizona

for 迴圈反覆執行 map 時,每個金鑰/值對會繫結到變數 abbrevfullName,這些變數在一個組中

(abbrev, fullName) <- states

當迴圈執行時,變數 abbrev 會指定給 map 中目前的金鑰,而變數 fullName 會指定給目前的 map

for 表達式

在先前的 for 迴圈範例中,這些迴圈都用於副作用,特別是使用 println 將這些值列印到 STDOUT。

您必須知道,您也可以建立傳回值的 for 表達式。您可以透過新增 yield 關鍵字和要傳回的表達式來建立 for 表達式,如下所示

val list =
  for (i <- 10 to 12)
  yield i * 2

// list: IndexedSeq[Int] = Vector(20, 22, 24)
val list =
  for i <- 10 to 12
  yield i * 2

// list: IndexedSeq[Int] = Vector(20, 22, 24)

在那之後 for 表達式執行,變數 list 是包含顯示值的 Vector。這是表達式運作的方式

  1. for 表達式開始在範圍 (10, 11, 12) 中的值上進行反覆運算。它首先對值 10 進行運算,將其乘以 2,然後產生該結果,即值 20
  2. 接下來,它對 11 進行運算,這是範圍中的第二個值。它將其乘以 2,然後產生值 22。你可以將這些產生的值視為累積在一個臨時儲存區中。
  3. 最後,迴圈從範圍中取得數字 12,將其乘以 2,產生數字 24。迴圈在此時完成,並產生最終結果,即 Vector(20, 22, 24)

雖然本節的目的是展示 for 表達式,但了解顯示的 for 表達式等同於這個 map 方法呼叫會有幫助

val list = (10 to 12).map(i => i * 2)

for 表達式可以在任何你需要遍歷集合中的所有元素,並對這些元素套用演算法以建立新清單的時候使用。

以下是一個範例,展示如何在 yield 之後使用程式碼區塊

val names = List("_olivia", "_walter", "_peter")

val capNames = for (name <- names) yield { 
  val nameWithoutUnderscore = name.drop(1)
  val capName = nameWithoutUnderscore.capitalize
  capName
}

// capNames: List[String] = List(Olivia, Walter, Peter)
val names = List("_olivia", "_walter", "_peter")

val capNames = for name <- names yield
  val nameWithoutUnderscore = name.drop(1)
  val capName = nameWithoutUnderscore.capitalize
  capName

// capNames: List[String] = List(Olivia, Walter, Peter)

for 表達式用作方法的主體

由於 for 表達式會產生結果,因此它可以用作傳回有用值的函式主體。此函式會傳回給定整數清單中介於 310 之間的所有值

def between3and10(xs: List[Int]): List[Int] =
  for {
    x <- xs
    if x >= 3
    if x <= 10
  } yield x

between3and10(List(1, 3, 7, 11))   // : List[Int] = List(3, 7)
def between3and10(xs: List[Int]): List[Int] =
  for
    x <- xs
    if x >= 3
    if x <= 10
  yield x

between3and10(List(1, 3, 7, 11))   // : List[Int] = List(3, 7)

while 迴圈

Scala while 迴圈語法如下所示

var i = 0

while (i < 3) {
  println(i)
  i += 1
}
var i = 0

while i < 3 do
  println(i)
  i += 1

match 表達式

模式比對是函數式程式語言的主要特色,而 Scala 包含一個功能強大的 match 表達式。

在最簡單的情況下,你可以使用 match 表達式,就像 Java 中的 switch 陳述式,根據整數值比對情況。請注意,這確實是一個表達式,因為它會評估為一個結果

// `i` is an integer
val day = i match {
  case 0 => "Sunday"
  case 1 => "Monday"
  case 2 => "Tuesday"
  case 3 => "Wednesday"
  case 4 => "Thursday"
  case 5 => "Friday"
  case 6 => "Saturday"
  case _ => "invalid day"   // the default, catch-all
}
// `i` is an integer
val day = i match
  case 0 => "Sunday"
  case 1 => "Monday"
  case 2 => "Tuesday"
  case 3 => "Wednesday"
  case 4 => "Thursday"
  case 5 => "Friday"
  case 6 => "Saturday"
  case _ => "invalid day"   // the default, catch-all

在此範例中,變數 i 會針對顯示的情況進行測試。如果介於 06 之間,day 會繫結到代表該星期幾的字串。否則,它會比對由字元 _ 所代表的萬用情況,而 day 會繫結到字串 "invalid day"

由於情況會按照編寫順序考慮,而且會使用第一個比對的情況,因此必須將比對任何值的預設情況放在最後。萬用情況之後的任何情況都會被警告為無法到達的情況。

在撰寫像這樣的簡單 match 表達式時,建議對變數 i 使用 @switch 註解。如果無法將 switch 編譯成效能更好的 tableswitchlookupswitch,此註解會提供編譯時期警告。

使用預設值

當你需要在 match 表達式中存取萬用預設值時,只要在 case 陳述式的左側提供變數名稱,而不是 _,然後在陳述式的右側視需要使用該變數名稱

i match {
  case 0 => println("1")
  case 1 => println("2")
  case what => println(s"You gave me: $what")
}
i match
  case 0 => println("1")
  case 1 => println("2")
  case what => println(s"You gave me: $what")

在模式中使用的名稱必須以小寫字母開頭。以大寫字母開頭的名稱不會引入變數,而是比對範圍內的某個值

val N = 42
i match {
  case 0 => println("1")
  case 1 => println("2")
  case N => println("42")
  case n => println(s"You gave me: $n" )
}
val N = 42
i match
  case 0 => println("1")
  case 1 => println("2")
  case N => println("42")
  case n => println(s"You gave me: $n" )

如果 i 等於 42,則 case N 會比對,而且會列印字串 "42"。它不會到達預設情況。

在一行中處理多個可能的比對

如前所述,match 表達式有許多功能。此範例顯示如何在每個 case 陳述式中使用多種可能的模式配對

val evenOrOdd = i match {
  case 1 | 3 | 5 | 7 | 9 => println("odd")
  case 2 | 4 | 6 | 8 | 10 => println("even")
  case _ => println("some other number")
}
val evenOrOdd = i match
  case 1 | 3 | 5 | 7 | 9 => println("odd")
  case 2 | 4 | 6 | 8 | 10 => println("even")
  case _ => println("some other number")

case 子句中使用 if 防護

您也可以在配對表達式的 case 中使用防護。在此範例中,第二個和第三個 case 都使用防護來配對多個整數值

i match {
  case 1 => println("one, a lonely number")
  case x if x == 2 || x == 3 => println("two’s company, three’s a crowd")
  case x if x > 3 => println("4+, that’s a party")
  case _ => println("i’m guessing your number is zero or less")
}
i match
  case 1 => println("one, a lonely number")
  case x if x == 2 || x == 3 => println("two’s company, three’s a crowd")
  case x if x > 3 => println("4+, that’s a party")
  case _ => println("i’m guessing your number is zero or less")

以下是另一個範例,顯示如何將給定值與數字範圍進行配對

i match {
  case a if 0 to 9 contains a => println(s"0-9 range: $a")
  case b if 10 to 19 contains b => println(s"10-19 range: $b")
  case c if 20 to 29 contains c => println(s"20-29 range: $c")
  case _ => println("Hmmm...")
}
i match
  case a if 0 to 9 contains a => println(s"0-9 range: $a")
  case b if 10 to 19 contains b => println(s"10-19 range: $b")
  case c if 20 to 29 contains c => println(s"20-29 range: $c")
  case _ => println("Hmmm...")

案例類別和配對表達式

您也可以從 case 類別中擷取欄位,以及已正確撰寫 apply/unapply 方法的類別,並在防護條件中使用這些欄位。以下是一個使用簡單 Person 案例類別的範例

case class Person(name: String)

def speak(p: Person) = p match {
  case Person(name) if name == "Fred" => println(s"$name says, Yubba dubba doo")
  case Person(name) if name == "Bam Bam" => println(s"$name says, Bam bam!")
  case _ => println("Watch the Flintstones!")
}

speak(Person("Fred"))      // "Fred says, Yubba dubba doo"
speak(Person("Bam Bam"))   // "Bam Bam says, Bam bam!"
case class Person(name: String)

def speak(p: Person) = p match
  case Person(name) if name == "Fred" => println(s"$name says, Yubba dubba doo")
  case Person(name) if name == "Bam Bam" => println(s"$name says, Bam bam!")
  case _ => println("Watch the Flintstones!")

speak(Person("Fred"))      // "Fred says, Yubba dubba doo"
speak(Person("Bam Bam"))   // "Bam Bam says, Bam bam!"

match 表達式用作方法的主體

由於 match 表達式會傳回值,因此它們可用作方法的主體。此方法將 Matchable 值作為輸入參數,並根據 match 表達式的結果傳回 Boolean

def isTruthy(a: Matchable) = a match {
  case 0 | "" | false => false
  case _              => true
}
def isTruthy(a: Matchable) = a match
  case 0 | "" | false => false
  case _              => true

輸入參數 a 定義為 Matchable 類型,這是可以執行模式配對的所有 Scala 類型的根。此方法是透過配對輸入來實作的,提供兩個案例:第一個案例檢查給定值是否為整數 0、空字串或 false,並在此情況下傳回 false。在預設情況下,我們會為任何其他值傳回 true。這些範例顯示此方法如何運作

isTruthy(0)      // false
isTruthy(false)  // false
isTruthy("")     // false
isTruthy(1)      // true
isTruthy(" ")    // true
isTruthy(2F)     // true

match 表達式用作方法的主體是很常見的用法。

配對表達式支援許多不同類型的模式

有許多不同形式的模式可用於撰寫 match 表達式。範例包括

  • 常數模式(例如 case 3 =>
  • 序列模式(例如 case List(els : _*) =>
  • 元組模式(例如 case (x, y) =>
  • 建構函數模式(例如 case Person(first, last) =>
  • 類型測試模式(例如 case p: Person =>

以下 pattern 方法顯示所有這些類型的模式,它採用 Matchable 類型的輸入參數並傳回 String

def pattern(x: Matchable): String = x match {

  // constant patterns
  case 0 => "zero"
  case true => "true"
  case "hello" => "you said 'hello'"
  case Nil => "an empty List"

  // sequence patterns
  case List(0, _, _) => "a 3-element list with 0 as the first element"
  case List(1, _*) => "list, starts with 1, has any number of elements"
  case Vector(1, _*) => "vector, starts w/ 1, has any number of elements"

  // tuple patterns
  case (a, b) => s"got $a and $b"
  case (a, b, c) => s"got $a, $b, and $c"

  // constructor patterns
  case Person(first, "Alexander") => s"Alexander, first name = $first"
  case Dog("Zeus") => "found a dog named Zeus"

  // type test patterns
  case s: String => s"got a string: $s"
  case i: Int => s"got an int: $i"
  case f: Float => s"got a float: $f"
  case a: Array[Int] => s"array of int: ${a.mkString(",")}"
  case as: Array[String] => s"string array: ${as.mkString(",")}"
  case d: Dog => s"dog: ${d.name}"
  case list: List[?] => s"got a List: $list"
  case m: Map[?, ?] => m.toString

  // the default wildcard pattern
  case _ => "Unknown"
}
def pattern(x: Matchable): String = x match

  // constant patterns
  case 0 => "zero"
  case true => "true"
  case "hello" => "you said 'hello'"
  case Nil => "an empty List"

  // sequence patterns
  case List(0, _, _) => "a 3-element list with 0 as the first element"
  case List(1, _*) => "list, starts with 1, has any number of elements"
  case Vector(1, _*) => "vector, starts w/ 1, has any number of elements"

  // tuple patterns
  case (a, b) => s"got $a and $b"
  case (a, b, c) => s"got $a, $b, and $c"

  // constructor patterns
  case Person(first, "Alexander") => s"Alexander, first name = $first"
  case Dog("Zeus") => "found a dog named Zeus"

  // type test patterns
  case s: String => s"got a string: $s"
  case i: Int => s"got an int: $i"
  case f: Float => s"got a float: $f"
  case a: Array[Int] => s"array of int: ${a.mkString(",")}"
  case as: Array[String] => s"string array: ${as.mkString(",")}"
  case d: Dog => s"dog: ${d.name}"
  case list: List[?] => s"got a List: $list"
  case m: Map[?, ?] => m.toString

  // the default wildcard pattern
  case _ => "Unknown"

try/catch/finally

與 Java 類似,Scala 有一個 try/catch/finally 結構,讓您可以捕捉並管理例外。為了保持一致性,Scala 使用與 match 表達式相同的語法,並支援對可能發生的不同例外進行模式配對。

在以下範例中,openAndReadAFile 是一個方法,它執行其名稱暗示的動作:它開啟一個檔案並讀取其中的文字,將結果指派給可變變數 text

var text = ""
try {
  text = openAndReadAFile(filename)
} catch {
  case fnf: FileNotFoundException => fnf.printStackTrace()
  case ioe: IOException => ioe.printStackTrace()
} finally {
  // close your resources here
  println("Came to the 'finally' clause.")
}
var text = ""
try
  text = openAndReadAFile(filename)
catch
  case fnf: FileNotFoundException => fnf.printStackTrace()
  case ioe: IOException => ioe.printStackTrace()
finally
  // close your resources here
  println("Came to the 'finally' clause.")

假設 openAndReadAFile 方法使用 Java java.io.* 類別來讀取檔案,且不會捕捉其例外,嘗試開啟並讀取檔案可能會導致 FileNotFoundExceptionIOException,且這兩個例外會在這個範例的 catch 區塊中被捕捉。

此頁面的貢獻者