Scala 3 — 書籍

集合類型

語言

此頁面示範常見的 Scala 3 集合及其隨附的方法。Scala 附帶許多集合類型,但您只要從其中幾個開始,稍後再根據需要使用其他類型,就能走很長一段路。同樣地,每個集合類型都有數十種方法讓您的生活更輕鬆,但您只要從其中幾個開始,就能達成許多目標。

因此,本節將介紹和示範您入門時最常見的類型和方法。當您需要更多彈性時,請參閱本節末尾的這些頁面以取得更多詳細資訊。

三種主要的集合類別

從高層級來看 Scala 集合,有三大類可供選擇

  • 序列是元素的順序集合,可能是索引(例如陣列)或線性(例如連結清單)
  • 對應包含一組鍵/值對,例如 Java Map、Python 字典或 Ruby Hash
  • 集合是唯一元素的無序集合

這些都是基本類型,並有特定用途的子類型,例如並行處理、快取和串流。除了這三個主要類別之外,還有其他有用的集合類型,包括範圍、堆疊和佇列。

集合階層

作為簡要概述,接下來的三個圖表顯示 Scala 集合中類別和特質的階層。

第一個圖表顯示套件 scala.collection 中的集合類型。這些都是高級抽象類別或特質,通常具有不可變可變的實作。

General collection hierarchy

此圖表顯示套件 scala.collection.immutable 中的所有集合

Immutable collection hierarchy

此圖表顯示套件 scala.collection.mutable 中的所有集合

Mutable collection hierarchy

在看到所有集合類型的詳細檢視後,以下各節將介紹一些您會定期使用的常見類型。

常見集合

您會定期使用的主要集合是

集合類型 不可變 可變 說明
清單   線性(連結清單),不可變序列
向量   已編製索引,不可變序列
LazyList   延遲不可變連結清單,其元素僅在需要時才計算;適用於大型或無限序列。
ArrayBuffer   可變,已編製索引序列的首選類型
ListBuffer   當您想要可變 List 時使用;通常轉換為 List
Map 包含鍵值對的迭代集合。
Set 沒有重複元素的迭代集合

如所示,MapSet 都具有不可變和可變版本。

每種類型的基礎知識在以下各節中說明。

在 Scala 中,緩衝區(例如 ArrayBufferListBuffer)是可以增長和縮小的序列。

關於不可變集合的說明

在以下各節中,每當使用不可變這個字時,可以安全地假設該類型是用於函式程式設計 (FP) 樣式。使用這些類型時,您不會修改集合;您會將函式方法套用至集合來建立新的結果。

選擇序列

在選擇序列(元素的順序集合)時,您有兩個主要的決定

  • 序列是否應編制索引(例如陣列),允許快速存取任何元素,或者是否應實作為線性連結清單?
  • 您要可變或不可變集合?

針對可變/不可變和編制索引/線性的組合,建議的一般用途「轉向」順序集合如下所示

類型/類別 不可變 可變
編制索引 向量 ArrayBuffer
線性(連結清單) 清單 ListBuffer

例如,如果您需要不可變的編制索引集合,一般來說您應使用 Vector。相反地,如果您需要可變的編制索引集合,請使用 ArrayBuffer

ListVector 通常用於撰寫函式樣式的程式碼。 ArrayBuffer 通常用於撰寫命令式樣式的程式碼。 ListBuffer 用於您混合樣式時,例如建立清單。

接下來的幾個部分將簡要示範 ListVectorArrayBuffer 類型。

清單

List 類型 是線性的不可變序列。這表示它是一個您無法修改的連結清單。任何時候您想要新增或移除 List 元素時,您會從現有的 List 建立新的 List

建立清單

以下是您建立初始 List 的方式

val ints = List(1, 2, 3)
val names = List("Joel", "Chris", "Ed")

// another way to construct a List
val namesAgain = "Joel" :: "Chris" :: "Ed" :: Nil

如果您喜歡,您也可以宣告 List 的類型,但通常沒有必要

val ints: List[Int] = List(1, 2, 3)
val names: List[String] = List("Joel", "Chris", "Ed")

一個例外是當您在集合中有混合類型時;在這種情況下,您可能想要明確指定其類型

val things: List[Any] = List(1, "two", 3.0)
val things: List[String | Int | Double] = List(1, "two", 3.0) // with union types
val thingsAny: List[Any] = List(1, "two", 3.0)                // with any

將元素新增至清單

由於 List 為不可變,因此無法新增新元素。相反地,您可以透過將元素置於現有 List 之前或之後來建立新的清單。例如,針對這個 List

val a = List(1, 2, 3)

在使用 List 時,使用 :: 將一個元素置於之前,並使用 ::: 將另一個 List 置於之前,如下所示

val b = 0 :: a              // List(0, 1, 2, 3)
val c = List(-1, 0) ::: a   // List(-1, 0, 1, 2, 3)

您也可以將元素附加到 List,但由於 List 為單向連結清單,因此通常只應將元素置於之前;將元素附加到 List 是相對較慢的操作,特別是在處理大型序列時。

提示:如果您想要將元素置於不可變序列之前和之後,請改用 Vector

由於 List 為連結清單,因此不應嘗試透過索引值存取大型清單的元素。例如,如果您有一個包含一百萬個元素的 List,則存取類似 myList(999_999) 的元素會花費相對較長的時間,因為該要求必須遍歷所有這些元素。如果您有一個大型集合,並想要透過索引存取元素,請改用 VectorArrayBuffer

如何記住方法名稱

現今的 IDE 對我們有很大的幫助,但記住這些方法名稱的方法之一是,將 : 字元視為序列所在的側邊,因此當您使用 +: 時,您知道清單必須在右側,如下所示

0 +: a

類似地,當您使用 :+ 時,您知道清單必須在左側

a :+ 4

還有更多技術性的思考方式,但這可能是記住方法名稱的一個有用的方法。

此外,這些符號方法名稱的一大優點是它們的一致性。其他不可變序列(例如 SeqVector)也使用相同的方法名稱。如果您願意,也可以使用非符號方法名稱來附加和預置元素。

如何迴圈處理清單

假設有一個名稱 List

val names = List("Joel", "Chris", "Ed")

您可以像這樣列印每個字串

for (name <- names) println(name)
for name <- names do println(name)

以下是 REPL 中的顯示方式

scala> for (name <- names) println(name)
Joel
Chris
Ed
scala> for name <- names do println(name)
Joel
Chris
Ed

使用 for 迴圈處理集合的一大優點是 Scala 的一致性,而且相同的做法適用於所有序列,包括 ArrayArrayBufferListSeqVectorMapSet 等。

一點歷史

對於有興趣了解一點歷史的人來說,Scala List 類似於 Lisp 程式語言 中的 List,後者最初於 1958 年制定。事實上,除了像這樣建立 List

val ints = List(1, 2, 3)

您還可以這樣建立完全相同的清單

val list = 1 :: 2 :: 3 :: Nil

REPL 顯示了這項運作方式

scala> val list = 1 :: 2 :: 3 :: Nil
list: List[Int] = List(1, 2, 3)

這是因為 List 是以 Nil 元素結尾的單向連結清單,而 ::List 方法,其運作方式類似於 Lisp 的「cons」運算子。

旁注:LazyList

Scala 集合還包括 LazyList,這是一個延遲不可變連結清單。它之所以稱為「延遲」或非嚴格,是因為它只在需要時才計算其元素。

您可以在 REPL 中看到 LazyList 有多麼延遲

val x = LazyList.range(1, Int.MaxValue)
x.take(1)      // LazyList(<not computed>)
x.take(5)      // LazyList(<not computed>)
x.map(_ + 1)   // LazyList(<not computed>)

在所有這些範例中,都沒有任何事情發生。事實上,在您強制執行(例如,透過呼叫其 foreach 方法)之前,都不會有任何事情發生

scala> x.take(1).foreach(println)
1

如需瞭解嚴格和非嚴格(延遲)集合的用途、好處和缺點,請參閱 Scala 2.13 集合的架構 頁面上的「嚴格」和「非嚴格」討論。

向量

Vector 是索引式不可變序列。說明中的「索引式」部分表示它提供隨機存取和更新,而且時間效率幾乎為常數,因此您可以透過索引值快速存取 Vector 元素,例如存取 listOfPeople(123_456_789)

一般來說,除了 (a) Vector 是索引式而 List 不是,以及 (b) List:: 方法之外,這兩種型別的運作方式相同,因此我們將快速瀏覽以下範例。

以下是建立 Vector 的幾個方法

val nums = Vector(1, 2, 3, 4, 5)

val strings = Vector("one", "two")

case class Person(name: String)
val people = Vector(
  Person("Bert"),
  Person("Ernie"),
  Person("Grover")
)

由於 Vector 是不可變的,因此無法新增元素。相反地,您可以透過附加或前置元素至現有的 Vector,來建立新的序列。以下範例說明如何將元素附加Vector

val a = Vector(1,2,3)         // Vector(1, 2, 3)
val b = a :+ 4                // Vector(1, 2, 3, 4)
val c = a ++ Vector(4, 5)     // Vector(1, 2, 3, 4, 5)

以下是前置元素的方法

val a = Vector(1,2,3)         // Vector(1, 2, 3)
val b = 0 +: a                // Vector(0, 1, 2, 3)
val c = Vector(-1, 0) ++: a   // Vector(-1, 0, 1, 2, 3)

除了快速的隨機存取和更新之外,Vector 還提供快速的附加和前置時間,因此您可以依需要使用這些功能。

請參閱 Collections Performance Characteristics,以取得 Vector 和其他集合的效能詳細資料。

最後,您可以使用 Vectorfor 迴圈中,就像 ListArrayBuffer 或任何其他序列一樣

scala> val names = Vector("Joel", "Chris", "Ed")
val names: Vector[String] = Vector(Joel, Chris, Ed)

scala> for (name <- names) println(name)
Joel
Chris
Ed
scala> val names = Vector("Joel", "Chris", "Ed")
val names: Vector[String] = Vector(Joel, Chris, Ed)

scala> for name <- names do println(name)
Joel
Chris
Ed

ArrayBuffer

當您在 Scala 應用程式中需要通用的可變索引序列時,請使用 ArrayBuffer。它是可變的,因此您可以變更其元素,也可以調整其大小。由於它是索引的,因此隨機存取元素的速度很快。

建立 ArrayBuffer

若要使用 ArrayBuffer,請先匯入

import scala.collection.mutable.ArrayBuffer

如果您需要從空的 ArrayBuffer 開始,只需指定其類型

var strings = ArrayBuffer[String]()
var ints = ArrayBuffer[Int]()
var people = ArrayBuffer[Person]()

如果您知道 ArrayBuffer 最終需要的近似大小,您可以使用初始大小來建立

// ready to hold 100,000 ints
val buf = new ArrayBuffer[Int](100_000)

若要建立具有初始元素的新 ArrayBuffer,只需指定其初始元素,就像 ListVector 一樣

val nums = ArrayBuffer(1, 2, 3)
val people = ArrayBuffer(
  Person("Bert"),
  Person("Ernie"),
  Person("Grover")
)

將元素新增至 ArrayBuffer

使用 +=++= 方法將新元素附加至 ArrayBuffer。或者,如果您偏好使用文字名稱的方法,您也可以使用 appendappendAllinsertinsertAllprependprependAll

以下是一些 +=++= 的範例

val nums = ArrayBuffer(1, 2, 3)   // ArrayBuffer(1, 2, 3)
nums += 4                         // ArrayBuffer(1, 2, 3, 4)
nums ++= List(5, 6)               // ArrayBuffer(1, 2, 3, 4, 5, 6)

從 ArrayBuffer 中移除元素

ArrayBuffer 是可變的,因此它有 -=--=clearremove 等方法。這些範例示範 -=--= 方法

val a = ArrayBuffer.range('a', 'h')   // ArrayBuffer(a, b, c, d, e, f, g)
a -= 'a'                              // ArrayBuffer(b, c, d, e, f, g)
a --= Seq('b', 'c')                   // ArrayBuffer(d, e, f, g)
a --= Set('d', 'e')                   // ArrayBuffer(f, g)

更新 ArrayBuffer 元素

透過重新指派想要的元素或使用 update 方法來更新 ArrayBuffer 中的元素

val a = ArrayBuffer.range(1,5)        // ArrayBuffer(1, 2, 3, 4)
a(2) = 50                             // ArrayBuffer(1, 2, 50, 4)
a.update(0, 10)                       // ArrayBuffer(10, 2, 50, 4)

Map

Map 是由鍵值對組成的可迭代集合。Scala 有可變和不可變的 Map 類型,而本節示範如何使用不可變Map

建立不可變 Map

像這樣建立不可變的 Map

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

取得 Map 後,你可以像這樣在 for 迴圈中遍歷它的元素

for ((k, v) <- states)  println(s"key: $k, value: $v")
for (k, v) <- states do println(s"key: $k, value: $v")

REPL 顯示了這項運作方式

scala> for ((k, v) <- states)  println(s"key: $k, value: $v")
key: AK, value: Alaska
key: AL, value: Alabama
key: AZ, value: Arizona
scala> for (k, v) <- states do println(s"key: $k, value: $v")
key: AK, value: Alaska
key: AL, value: Alabama
key: AZ, value: Arizona

存取 Map 元素

透過在括號中指定想要的鍵值來存取 map 元素

val ak = states("AK")   // ak: String = Alaska
val al = states("AL")   // al: String = Alabama

實際上,你還會使用 keyskeySetkeysIteratorfor 迴圈和 map 等高階函數來處理 Map 鍵和值。

將元素新增到 Map

使用 +++ 將元素新增到不可變 map,記得將結果指派給新的變數

val a = Map(1 -> "one")    // a: Map(1 -> one)
val b = a + (2 -> "two")   // b: Map(1 -> one, 2 -> two)
val c = b ++ Seq(
  3 -> "three",
  4 -> "four"
)
// c: Map(1 -> one, 2 -> two, 3 -> three, 4 -> four)

從 Map 中移除元素

使用 --- 和要移除的鍵值從不可變 map 中移除元素,記得將結果指派給新的變數

val a = Map(
  1 -> "one",
  2 -> "two",
  3 -> "three",
  4 -> "four"
)

val b = a - 4       // b: Map(1 -> one, 2 -> two, 3 -> three)
val c = a - 4 - 3   // c: Map(1 -> one, 2 -> two)

更新 Map 元素

要更新不可變 map 中的元素,請使用 updated 方法(或 + 算子),同時將結果指派給新的變數

val a = Map(
  1 -> "one",
  2 -> "two",
  3 -> "three"
)

val b = a.updated(3, "THREE!")   // b: Map(1 -> one, 2 -> two, 3 -> THREE!)
val c = a + (2 -> "TWO...")      // c: Map(1 -> one, 2 -> TWO..., 3 -> three)

遍歷 Map

如前所示,這是使用 for 迴圈手動遍歷地圖中元素的常見方式

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

for ((k, v) <- states) println(s"key: $k, value: $v")
val states = Map(
  "AK" -> "Alaska",
  "AL" -> "Alabama",
  "AZ" -> "Arizona"
)

for (k, v) <- states do println(s"key: $k, value: $v")

話雖如此,有許多方法可以使用地圖中的鍵和值。常見的 Map 方法包括 foreachmapkeysvalues

Scala 有更多更專業的 Map 類型,包括 CollisionProofHashMapHashMapLinkedHashMapListMapSortedMapTreeMapWeakHashMap 等。

使用集合

Scala Set 是沒有重複元素的可迭代集合。

Scala 同時有可變和不可變的 Set 類型。本節說明不可變Set

建立集合

建立新的空集合如下

val nums = Set[Int]()
val letters = Set[Char]()

建立包含初始資料的集合如下

val nums = Set(1, 2, 3, 3, 3)           // Set(1, 2, 3)
val letters = Set('a', 'b', 'c', 'c')   // Set('a', 'b', 'c')

將元素新增至集合

使用 +++ 將元素新增至不可變的 Set,並記得將結果指定給新的變數

val a = Set(1, 2)                // Set(1, 2)
val b = a + 3                    // Set(1, 2, 3)
val c = b ++ Seq(4, 1, 5, 5)     // HashSet(5, 1, 2, 3, 4)

請注意,當您嘗試新增重複的元素時,它們會被靜默地捨棄。

另外請注意,元素的迭代順序是任意的。

從集合中刪除元素

使用 --- 從不可變的集合中移除元素,並再次將結果指定給新的變數

val a = Set(1, 2, 3, 4, 5)   // HashSet(5, 1, 2, 3, 4)
val b = a - 5                // HashSet(1, 2, 3, 4)
val c = b -- Seq(3, 4)       // HashSet(1, 2)

範圍

Scala Range 通常用於填充資料結構和迭代 for 迴圈。這些 REPL 範例說明如何建立範圍

1 to 5         // Range(1, 2, 3, 4, 5)
1 until 5      // Range(1, 2, 3, 4)
1 to 10 by 2   // Range(1, 3, 5, 7, 9)
'a' to 'c'     // NumericRange(a, b, c)

您可以使用範圍來填充集合

val x = (1 to 5).toList     // List(1, 2, 3, 4, 5)
val x = (1 to 5).toBuffer   // ArrayBuffer(1, 2, 3, 4, 5)

它們也用於 for 迴圈中

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

上也有 range 方法

Vector.range(1, 5)       // Vector(1, 2, 3, 4)
List.range(1, 10, 2)     // List(1, 3, 5, 7, 9)
Set.range(1, 10)         // HashSet(5, 1, 6, 9, 2, 7, 3, 8, 4)

執行測試時,範圍也很適合用於產生測試集合

val evens = (0 to 10 by 2).toList     // List(0, 2, 4, 6, 8, 10)
val odds = (1 to 10 by 2).toList      // List(1, 3, 5, 7, 9)
val doubles = (1 to 5).map(_ * 2.0)   // Vector(2.0, 4.0, 6.0, 8.0, 10.0)

// create a Map
val map = (1 to 3).map(e => (e,s"$e")).toMap
    // map: Map[Int, String] = Map(1 -> "1", 2 -> "2", 3 -> "3")

更多詳細資訊

當您需要進一步了解專業集合時,請參閱下列資源

此頁面的貢獻者