Scala 3 — 書籍

集合方法

語言

Scala 集合的一大優勢是它們內建了數十種方法,而且這些方法在不可變和可變集合類型中都能一致使用。這樣的好處是您不再需要每次使用集合時都撰寫自訂 for 迴圈,而且當您從一個專案移到另一個專案時,您會發現使用這些相同的方法,而不是更多自訂 for 迴圈。

數十種方法可用,因此並非所有方法都顯示於此。相反地,只顯示一些最常使用的方法,包含

  • map
  • filter
  • foreach
  • head
  • tail
  • taketakeWhile
  • dropdropWhile
  • reduce

下列方法適用於所有序列類型,包含 ListVectorArrayBuffer 等,但這些範例使用 List,除非另有說明。

非常重要的注意事項是,List 上的任何方法都不會變異清單。它們都以函數式樣式運作,表示它們會傳回包含修改結果的新集合。

常見方法範例

為了讓您概覽以下各節中會看到的內容,這些範例顯示一些最常使用的集合方法。首先,以下是未使用的 lambda 的一些方法

val a = List(10, 20, 30, 40, 10)      // List(10, 20, 30, 40, 10)

a.distinct                            // List(10, 20, 30, 40)
a.drop(2)                             // List(30, 40, 10)
a.dropRight(2)                        // List(10, 20, 30)
a.head                                // 10
a.headOption                          // Some(10)
a.init                                // List(10, 20, 30, 40)
a.intersect(List(19,20,21))           // List(20)
a.last                                // 10
a.lastOption                          // Some(10)
a.slice(2,4)                          // List(30, 40)
a.tail                                // List(20, 30, 40, 10)
a.take(3)                             // List(10, 20, 30)
a.takeRight(2)                        // List(40, 10)

高階函數和 lambda

接下來,我們將顯示一些常用的高階函數 (HOF),它們接受 lambda(匿名函數)。首先,以下是 lambda 語法的一些變異,從最長的形式開始,逐步進行到最簡潔的形式

// these functions are all equivalent and return
// the same data: List(10, 20, 10)

a.filter((i: Int) => i < 25)   // 1. most explicit form
a.filter((i) => i < 25)        // 2. `Int` is not required
a.filter(i => i < 25)          // 3. the parens are not required
a.filter(_ < 25)               // 4. `i` is not required

在那些編號範例中

  1. 第一個範例顯示最長的形式。這種冗長很少需要,而且只有在最複雜的用法中才需要。
  2. 編譯器知道 a 包含 Int,因此不需要在此處重新陳述。
  3. 當您只有一個參數時,例如 i,則不需要括號。
  4. 當您有一個單一參數,且它只出現在匿名函式中一次時,您可以用 _ 取代該參數。

匿名函式 提供了更多詳細資訊和範例,說明與縮短 lambda 運算式相關的規則。

現在您已經看過簡潔的形式,以下是使用簡短 lambda 語法形式的其他 HOF 範例

a.dropWhile(_ < 25)   // List(30, 40, 10)
a.filter(_ > 100)     // List()
a.filterNot(_ < 25)   // List(30, 40)
a.find(_ > 20)        // Some(30)
a.takeWhile(_ < 30)   // List(10, 20)

請務必注意,HOF 也接受方法和函式作為參數,而不仅仅是 lambda 運算式。以下是 map HOF 的一些範例,它使用名為 double 的方法。再次顯示 lambda 語法形式的幾個變體

def double(i: Int) = i * 2

// these all return `List(20, 40, 60, 80, 20)`
a.map(i => double(i))
a.map(double(_))
a.map(double)

在最後一個範例中,當匿名函式包含一個需要單一引數的函式呼叫時,您不必命名引數,因此甚至不需要 _

最後,您可以根據需要組合 HOF 來解決問題

// yields `List(100, 200)`
a.filter(_ < 40)
 .takeWhile(_ < 30)
 .map(_ * 10)

範例資料

以下各節中的範例使用這些清單

val oneToTen = (1 to 10).toList
val names = List("adam", "brandy", "chris", "david")

map

map 方法會逐一檢視現有清單中的每個元素,將您提供的函式套用至每個元素,一次一個;然後傳回一個包含所有已修改元素的新清單。

以下是將 map 方法套用至 oneToTen 清單的範例

scala> val doubles = oneToTen.map(_ * 2)
doubles: List[Int] = List(2, 4, 6, 8, 10, 12, 14, 16, 18, 20)

您也可以使用長格式撰寫匿名函式,如下所示

scala> val doubles = oneToTen.map(i => i * 2)
doubles: List[Int] = List(2, 4, 6, 8, 10, 12, 14, 16, 18, 20)

不過,在本課程中,我們將始終使用第一個較短的格式。

以下是將 map 方法套用至 oneToTennames 清單的更多範例

scala> val capNames = names.map(_.capitalize)
capNames: List[String] = List(Adam, Brandy, Chris, David)

scala> val nameLengthsMap = names.map(s => (s, s.length)).toMap
nameLengthsMap: Map[String, Int] = Map(adam -> 4, brandy -> 6, chris -> 5, david -> 5)

scala> val isLessThanFive = oneToTen.map(_ < 5)
isLessThanFive: List[Boolean] = List(true, true, true, true, false, false, false, false, false, false)

如最後兩個範例所示,使用 map 回傳與原始類型不同的集合是完全合法的(且常見)。

filter

filter 方法會建立一個新的清單,其中包含符合所提供謂詞的元素。謂詞或條件是一種會回傳 Booleantruefalse)的函式。以下是一些範例

scala> val lessThanFive = oneToTen.filter(_ < 5)
lessThanFive: List[Int] = List(1, 2, 3, 4)

scala> val evens = oneToTen.filter(_ % 2 == 0)
evens: List[Int] = List(2, 4, 6, 8, 10)

scala> val shortNames = names.filter(_.length <= 4)
shortNames: List[String] = List(adam)

集合中的函數方法有一項優點,就是您可以將它們串連在一起以解決問題。例如,這個範例顯示如何串連 filtermap

oneToTen.filter(_ < 4).map(_ * 10)

REPL 會顯示結果

scala> oneToTen.filter(_ < 4).map(_ * 10)
val res1: List[Int] = List(10, 20, 30)

foreach

foreach 方法用於迴圈處理集合中的所有元素。請注意,foreach 用於副作用,例如列印資訊。以下是 names 清單的範例

scala> names.foreach(println)
adam
brandy
chris
david

head 方法來自 Lisp 和其他較早的函數式程式語言。它用於存取清單的第一個元素(head 元素)

oneToTen.head   // 1
names.head      // adam

由於 String 可以視為字元序列,因此您也可以將它視為清單。這就是 head 在這些字串上運作的方式

"foo".head   // 'f'
"bar".head   // 'b'

head 是一個很棒的方法,但作為一個警告,它在一個空的集合中呼叫時也會拋出一個例外

val emptyList = List[Int]()   // emptyList: List[Int] = List()
emptyList.head                // java.util.NoSuchElementException: head of empty list

因此,您可能希望使用 headOption 而不是 head,特別是在以函數式風格編程時

emptyList.headOption          // None

如所示,它不會拋出例外,它只返回具有值 None 的類型 Option。您可以在 函數式編程 章節中了解有關此編程風格的更多信息。

tail

tail 方法也來自 Lisp,它用於列印清單中頭部元素之後的每個元素。一些範例說明了這一點

oneToTen.head   // 1
oneToTen.tail   // List(2, 3, 4, 5, 6, 7, 8, 9, 10)

names.head      // adam
names.tail      // List(brandy, chris, david)

就像 headtail 也適用於字串

"foo".tail   // "oo"
"bar".tail   // "ar"

如果清單為空,tail 會拋出一個 java.lang.UnsupportedOperationException,所以就像 headheadOption,還有一個 tailOption 方法,在函數式編程中較受青睞。

清單也可以匹配,因此您可以撰寫這樣的表達式

val x :: xs = names

將該代碼放入 REPL 中會顯示 x 被指定為清單的頭部,而 xs 被指定為尾部

scala> val x :: xs = names
val x: String = adam
val xs: List[String] = List(brandy, chris, david)

這種模式匹配在許多情況下很有用,例如使用遞迴撰寫 sum 方法

def sum(list: List[Int]): Int = list match {
  case Nil => 0
  case x :: xs => x + sum(xs)
}
def sum(list: List[Int]): Int = list match
  case Nil => 0
  case x :: xs => x + sum(xs)

taketakeRighttakeWhile

taketakeRighttakeWhile 方法提供了一個很好的方式來「擷取」清單中您想要用來建立新清單的元素。這是 taketakeRight

oneToTen.take(1)        // List(1)
oneToTen.take(2)        // List(1, 2)

oneToTen.takeRight(1)   // List(10)
oneToTen.takeRight(2)   // List(9, 10)

請注意這些方法如何處理「邊緣」情況,例如我們要求的元素多於序列中的元素,或要求零個元素

oneToTen.take(Int.MaxValue)        // List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
oneToTen.takeRight(Int.MaxValue)   // List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
oneToTen.take(0)                   // List()
oneToTen.takeRight(0)              // List()

這是 takeWhile,它使用謂詞函式

oneToTen.takeWhile(_ < 5)       // List(1, 2, 3, 4)
names.takeWhile(_.length < 5)   // List(adam)

dropdropRightdropWhile

dropdropRightdropWhile 本質上與它們的「take」對應項相反,從清單中刪除元素。以下是一些範例

oneToTen.drop(1)        // List(2, 3, 4, 5, 6, 7, 8, 9, 10)
oneToTen.drop(5)        // List(6, 7, 8, 9, 10)

oneToTen.dropRight(8)   // List(1, 2)
oneToTen.dropRight(7)   // List(1, 2, 3)

再次注意這些方法如何處理邊緣情況

oneToTen.drop(Int.MaxValue)        // List()
oneToTen.dropRight(Int.MaxValue)   // List()
oneToTen.drop(0)                   // List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
oneToTen.dropRight(0)              // List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

這是 dropWhile,它使用謂詞函式

oneToTen.dropWhile(_ < 5)       // List(5, 6, 7, 8, 9, 10)
names.dropWhile(_ != "chris")   // List(chris, david)

reduce

當您聽到「map reduce」這個術語時,「reduce」部分是指像 reduce 這樣的函式。它會採用一個函式(或匿名函式),並將該函式套用至清單中連續的元素。

說明 reduce 的最佳方式是建立一個您可以傳遞給它的簡易輔助函式。例如,這是 add 函式,它會將兩個整數加總,並提供一些不錯的偵錯輸出

def add(x: Int, y: Int): Int = {
  val theSum = x + y
  println(s"received $x and $y, their sum is $theSum")
  theSum
}
def add(x: Int, y: Int): Int =
  val theSum = x + y
  println(s"received $x and $y, their sum is $theSum")
  theSum

假設有方法和此清單

val a = List(1,2,3,4)

當您將 add 方法傳遞到 reduce 中時,就會發生這種情況

scala> a.reduce(add)
received 1 and 2, their sum is 3
received 3 and 3, their sum is 6
received 6 and 4, their sum is 10
res0: Int = 10

如結果所示,reduce 使用 add 將清單 a 縮減為單一值,在本例中,為清單中整數的總和。

一旦您習慣了 reduce,您就可以像這樣撰寫「總和」演算法

scala> a.reduce(_ + _)
res0: Int = 10

類似地,「乘積」演算法如下所示

scala> a.reduce(_ * _)
res1: Int = 24

關於 reduce,需要知道的一個重要概念是,正如其名稱所暗示的那樣,它用於將集合縮減為單一值。

甚至更多

Scala 集合類型中有數十種其他方法,讓您永遠不需要再撰寫另一個 for 迴圈。請參閱 可變和不可變集合Scala 集合架構,以取得更多有關 Scala 集合的詳細資訊。

最後,如果您在 Scala 專案中使用 Java 程式碼,則可以將 Java 集合轉換為 Scala 集合。這樣一來,您可以在 for 表達式中使用這些集合,並且還可以利用 Scala 的函式集合方法。請參閱 與 Java 互動 部分以取得更多詳細資訊。

此頁面的貢獻者