Scala 集合的一大優勢是它們內建了數十種方法,而且這些方法在不可變和可變集合類型中都能一致使用。這樣的好處是您不再需要每次使用集合時都撰寫自訂 for
迴圈,而且當您從一個專案移到另一個專案時,您會發現使用這些相同的方法,而不是更多自訂 for
迴圈。
有數十種方法可用,因此並非所有方法都顯示於此。相反地,只顯示一些最常使用的方法,包含
map
filter
foreach
head
tail
take
、takeWhile
drop
、dropWhile
reduce
下列方法適用於所有序列類型,包含 List
、Vector
、ArrayBuffer
等,但這些範例使用 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
在那些編號範例中
- 第一個範例顯示最長的形式。這種冗長很少需要,而且只有在最複雜的用法中才需要。
- 編譯器知道
a
包含Int
,因此不需要在此處重新陳述。 - 當您只有一個參數時,例如
i
,則不需要括號。 - 當您有一個單一參數,且它只出現在匿名函式中一次時,您可以用
_
取代該參數。
匿名函式 提供了更多詳細資訊和範例,說明與縮短 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
方法套用至 oneToTen
和 names
清單的更多範例
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
方法會建立一個新的清單,其中包含符合所提供謂詞的元素。謂詞或條件是一種會回傳 Boolean
(true
或 false
)的函式。以下是一些範例
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)
集合中的函數方法有一項優點,就是您可以將它們串連在一起以解決問題。例如,這個範例顯示如何串連 filter
和 map
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
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)
就像 head
,tail
也適用於字串
"foo".tail // "oo"
"bar".tail // "ar"
如果清單為空,tail
會拋出一個 java.lang.UnsupportedOperationException,所以就像 head
和 headOption
,還有一個 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)
take
、takeRight
、takeWhile
take
、takeRight
和 takeWhile
方法提供了一個很好的方式來「擷取」清單中您想要用來建立新清單的元素。這是 take
和 takeRight
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)
drop
、dropRight
、dropWhile
drop
、dropRight
和 dropWhile
本質上與它們的「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 互動 部分以取得更多詳細資訊。