集合

迭代器

語言

迭代器並非集合,而是一種逐一存取集合元素的方法。迭代器 it 的兩個基本操作為 nexthasNext。呼叫 it.next() 會傳回迭代器的下一個元素並推進迭代器的狀態。再次對同一個迭代器呼叫 next,會產生比先前傳回元素再下一個的元素。如果沒有更多元素可以傳回,呼叫 next 會擲出 NoSuchElementException。您可以使用 IteratorhasNext 方法來找出是否還有更多元素可以傳回。

「逐步執行」迭代器 it 傳回的所有元素最直接的方法是使用 while 迴圈

while (it.hasNext)
  println(it.next())
while it.hasNext do
  println(it.next())

Scala 中的迭代器也提供類似於 IterableSeq 類別中大多數方法的類比。例如,它們提供一個 foreach 方法,用於對迭代器傳回的每個元素執行特定程序。使用 foreach,上面的迴圈可以縮寫為

it.foreach(println)

一如往常,for-expression 可以用作涉及 foreachmapwithFilterflatMap 的 expression 的替代語法,因此列印迭代器傳回的所有元素的另一種方法是

for (elem <- it) println(elem)
for elem <- it do println(elem)

迭代器上的 foreach 方法和可迭代集合上的相同方法之間有一個重要的差異:當在迭代器上呼叫時,foreach 會在完成時將迭代器保留在其結尾。因此,再次在同一個迭代器上呼叫 next 會因 NoSuchElementException 而失敗。相反,當在集合上呼叫時,foreach 會讓集合中的元素數量保持不變(除非傳遞的函式新增或移除元素,但這並不建議,因為這可能會導致令人驚訝的結果)。

IteratorIterable 共有的其他運算具有相同的屬性。例如,迭代器提供 map 方法,它會傳回一個新的迭代器

scala> val it = Iterator("a", "number", "of", "words")
val it: Iterator[java.lang.String] = <iterator>

scala> it.map(_.length)
val res1: Iterator[Int] = <iterator>

scala> it.hasNext
val res2: Boolean = true

scala> res1.foreach(println)
1
6
2
5

scala> it.hasNext
val res4: Boolean = false

如你所見,在呼叫 it.map 之後,it 迭代器並未進展到其結尾,但遍歷呼叫 res1.foreach 所產生的迭代器也會遍歷 it 並將其進展到其結尾。

另一個範例是 dropWhile 方法,它可用於尋找具有特定屬性的迭代器的第一個元素。例如,若要尋找上面迭代器中第一個至少有兩個字元的字詞,你可以寫

scala> val it = Iterator("a", "number", "of", "words")
val it: Iterator[java.lang.String] = <iterator>

scala> it.dropWhile(_.length < 2)
val res4: Iterator[java.lang.String] = <iterator>

scala> res4.next()
val res5: java.lang.String = number

再次注意,it 已由呼叫 dropWhile 而變更:它現在指向清單中的第二個字詞「number」。事實上,itdropWhile 傳回的結果 res4 將會傳回完全相同的元素順序。

要迴避此行為的一種方法是 duplicate 基礎反覆運算器,而不是直接呼叫其方法。產生的兩個反覆運算器將會各傳回與基礎反覆運算器 it 完全相同的元素

scala> val (words, ns) = Iterator("a", "number", "of", "words").duplicate
val words: Iterator[String] = <iterator>
val ns: Iterator[String] = <iterator>

scala> val shorts = words.filter(_.length < 3).toList
val shorts: List[String] = List(a, of)

scala> val count = ns.map(_.length).sum
val count: Int = 14

這兩個反覆運算器獨立運作:推進一個不會影響另一個,因此每個反覆運算器都可以透過呼叫任意方法而被破壞性修改。這會產生反覆運算元素兩次的錯覺,但效果是透過內部緩衝來達成的。如同往常,基礎反覆運算器 it 無法直接使用,且必須捨棄。

總之,如果在呼叫迭代器上的方法後,不再存取迭代器,則迭代器會像集合一樣運作。Scala 集合函式庫透過抽象 IterableOnce 明確表達這一點,它是 IterableIterator 的共同超類別。IterableOnce[A] 只有兩個方法:iterator: Iterator[A]knownSize: Int。如果 IterableOnce 物件實際上是 Iterator,則其 iterator 作業會在目前狀態下,永遠回傳它自己,但如果它是 Iterable,則其 iterator 作業會永遠回傳新的 IteratorIterableOnce 的常見使用案例,是作為方法的引數類型,這些方法可以將迭代器或集合作為引數。一個範例是 Iterable 類別中的追加方法 concat。它採用 IterableOnce 參數,因此您可以追加來自迭代器或集合的元素。

以下是迭代器上所有作業的摘要。

Iterator 類別中的作業

作業內容 作業功能
抽象方法  
it.next() 回傳迭代器上的下一個元素,並進到下一個元素。
it.hasNext 如果 it 可以傳回另一個元素,則傳回 true
變異  
it.buffered 一個緩衝迭代器,傳回 it 的所有元素。
it.grouped(size) 一個迭代器,以固定大小的序列「區塊」產生 it 傳回的元素。
it.sliding(size) 一個迭代器,以表示滑動固定大小視窗的序列產生 it 傳回的元素。
複製  
it.duplicate 一對迭代器,各自獨立傳回 it 的所有元素。
新增  
it.concat(jt)
it ++ jt
一個迭代器,傳回迭代器 it 傳回的所有元素,接著傳回迭代器 jt 傳回的所有元素。
it.padTo(len, x) 一個迭代器,先傳回 it 的所有元素,然後接著傳回 x 的複本,直到總共傳回 len 個元素。
對應  
it.map(f) 一個迭代器,從 it 傳回的每個元素套用函數 f 而取得。
it.flatMap(f) 一個迭代器,從 it 中的每個元素套用迭代器值函數 f 而取得,並附加結果。
it.collect(f) 將部分函數 f 套用至 it 中每個已定義的元素,並收集結果而取得的迭代器。
轉換  
it.toArray it 傳回的元素收集至陣列中。
it.toList it 傳回的元素收集至清單中。
it.toIterable it 傳回的元素收集至可迭代物件中。
it.toSeq it 傳回的元素收集至序列中。
it.toIndexedSeq it 傳回的元素收集至索引序列中。
it.toLazyList it 傳回的元素收集至延遲清單中。
it.toSet it 傳回的元素收集至集合中。
it.toMap it 傳回的鍵/值對收集至映射中。
複製  
it.copyToArray(arr, s, n) it 傳回的最多 n 個元素複製至陣列 arr,從索引 s 開始。最後兩個參數為選用參數。
大小資訊  
it.isEmpty 測試迭代器是否為空(與 hasNext 相反)。
it.nonEmpty 測試集合是否包含元素(hasNext 的別名)。
it.size it 傳回的元素數量。注意:此操作後 it 將會在結尾處!
it.length it.size 相同。
it.knownSize 元素數量,如果已知且不修改迭代器的狀態,否則為 -1
元素檢索索引搜尋  
it.find(p) 包含 it 回傳的第一個符合 p 的元素,或如果沒有符合的元素則為 None。注意:迭代器會進展到該元素之後,或如果找不到元素則進展到最後。
it.indexOf(x) it 回傳的第一個等於 x 的元素的索引。注意:迭代器會進展到該元素的位置之後。
it.indexWhere(p) it 回傳的第一個符合 p 的元素的索引。注意:迭代器會進展到該元素的位置之後。
子迭代器  
it.take(n) 回傳 it 的前 n 個元素的迭代器。注意:它會進展到 n 個元素之後的位置,或如果包含的元素少於 n 個元素則進展到最後。
it.drop(n) it 的第 (n+1) 個元素開始的迭代器。注意:it 會進展到相同的位置。
it.slice(m,n) 回傳從 it 回傳的元素的切片,從第 m 個元素開始,在第 n 個元素之前結束。
it.takeWhile(p) 只要條件 p 為真,就會從 it 回傳元素的迭代器。
it.dropWhile(p) 一個迭代器跳過 it 中的元素,只要條件 ptrue,並傳回剩餘的元素。
it.filter(p) 一個迭代器傳回 it 中滿足條件 p 的所有元素。
it.withFilter(p) it filter p 相同。需要這樣做才能在 for-expressions 中使用迭代器。
it.filterNot(p) 一個迭代器傳回 it 中不滿足條件 p 的所有元素。
it.distinct 一個迭代器傳回 it 中不重複的元素。
細分  
it.partition(p) it 分割成一對兩個迭代器:一個傳回 it 中滿足謂詞 p 的所有元素,另一個傳回 it 中不滿足謂詞 p 的所有元素。
it.span(p) it 分割成一對兩個迭代器:一個傳回 it 的前綴中滿足謂詞 p 的所有元素,另一個傳回 it 的所有剩餘元素。
元素條件  
it.forall(p) 一個布林值,表示謂詞 p 是否對 it 傳回的所有元素成立。
it.exists(p) 布林值,指示謂詞 p 是否對 it 中的某些元素成立。
it.count(p) 滿足謂詞 pit 中的元素數量。
折疊  
it.foldLeft(z)(op) it 傳回的連續元素之間套用二元運算 op,由左至右,並從 z 開始。
it.foldRight(z)(op) it 傳回的連續元素之間套用二元運算 op,由右至左,並從 z 開始。
it.reduceLeft(op) 在非空反覆運算 it 傳回的連續元素之間套用二元運算 op,由左至右。
it.reduceRight(op) 在非空反覆運算 it 傳回的連續元素之間套用二元運算 op,由右至左。
特定折疊  
it.sum 反覆運算 it 傳回的數字元素值的總和。
it.product 反覆運算 it 傳回的數字元素值的乘積。
it.min 反覆運算 it 傳回的有序元素值的最小值。
it.max 反覆運算 it 傳回的有序元素值的最大值。
拉鍊  
it.zip(jt) 從反覆運算 itjt 傳回的對應元素組成的對反覆運算。
it.zipAll(jt, x, y) 一個迭代器,由迭代器 itjt 傳回的對應元素組成,其中較短的迭代器會附加元素 xy 來與較長的迭代器相符。
it.zipWithIndex 一個迭代器,由 it 傳回的元素與其索引組成。
更新  
it.patch(i, jt, r) it 產生的迭代器,它從 i 開始,用修補程式迭代器 jt 取代 r 個元素。
比較  
it.sameElements(jt) 一個測試,用來檢查迭代器 itjt 是否以相同的順序傳回相同的元素。注意:此操作後使用迭代器是不確定的,且可能會變更。
字串  
it.addString(b, start, sep, end) 將一個字串新增到 StringBuilder b,其中會顯示 it 傳回的所有元素,它們位於分隔符號 sep 之間,並用字串 startend 包圍。 startsepend 都是選用的。
it.mkString(start, sep, end) 將集合轉換成一個字串,其中會顯示 it 傳回的所有元素,它們位於分隔符號 sep 之間,並用字串 startend 包圍。 startsepend 都是選用的。

惰性

與直接對具體集合(例如 List)進行的操作不同,對 Iterator 進行的操作是延遲的。

延遲操作不會立即計算其所有結果。相反,它會在個別請求時計算結果。

因此,表達式 (1 to 10).iterator.map(println) 不會在螢幕上列印任何內容。在此情況下,map 方法並未將其參數函式套用至範圍中的值,而是會傳回一個新的 Iterator,而這個 Iterator 會在個別請求時執行此動作。將 .toList 新增至該表達式的結尾會實際列印元素。

這會導致像 mapfilter 之類的方法不一定會將其參數函式套用至所有輸入元素。例如,表達式 (1 to 10).iterator.map(println).take(5).toList 僅會列印值 15,因為這些值是唯一會從 map 傳回的 Iterator 中請求的。

這就是為什麼僅將純函式用作 mapfilterfold 和類似方法的參數非常重要的原因之一。請記住,純函式沒有副作用,因此通常不會在 map 中使用 println。使用 println 是為了展示延遲,因為它通常不會在純函式中顯示。

儘管懶惰通常不可見,但它仍然有價值,因為它可以防止不必要的計算發生,並且可以處理無窮序列,如下所示

def zipWithIndex[A](i: Iterator[A]): Iterator[(Int, A)] =
  Iterator.from(0).zip(i)

緩衝迭代器

有時你想要一個可以「向前看」的迭代器,以便你可以檢查要傳回的下一個元素,而不用跳過該元素。例如,考慮從傳回字串序列的迭代器中跳過前導空字串的任務。你可能會想寫以下內容

def skipEmptyWordsNOT(it: Iterator[String]) =
  while (it.next().isEmpty) {}
def skipEmptyWordsNOT(it: Iterator[String]) =
  while it.next().isEmpty do ()

但是仔細查看這段程式碼,很明顯這是錯誤的:程式碼確實會跳過前導空字串,但它也會推進it超過第一個非空字串!

解決此問題的方法是使用緩衝迭代器。類別 BufferedIteratorIterator 的子類別,它提供了一個額外的函數 head。在緩衝迭代器上呼叫 head 會傳回它的第一個元素,但不會推進迭代器。使用緩衝迭代器,可以寫成如下方式來跳過空字詞。

def skipEmptyWords(it: BufferedIterator[String]) =
  while (it.head.isEmpty) { it.next() }
def skipEmptyWords(it: BufferedIterator[String]) =
  while it.head.isEmpty do it.next()

每個迭代器都可以透過呼叫其 buffered 函數來轉換為緩衝迭代器。以下是一個範例

scala> val it = Iterator(1, 2, 3, 4)
val it: Iterator[Int] = <iterator>

scala> val bit = it.buffered
val bit: scala.collection.BufferedIterator[Int] = <iterator>

scala> bit.head
val res10: Int = 1

scala> bit.next()
val res11: Int = 1

scala> bit.next()
val res12: Int = 2

scala> bit.headOption
val res13: Option[Int] = Some(3)

請注意,在緩衝迭代器 bit 上呼叫 head 不會推進它。因此,後續呼叫 bit.next() 傳回的值與 bit.head 相同。

與往常一樣,基礎迭代器不得直接使用,並且必須捨棄。

緩衝迭代器僅在呼叫 head 時緩衝下一個元素。其他衍生迭代器,例如由 duplicatepartition 產生的迭代器,可能會緩衝基礎迭代器的任意子序列。但是迭代器可以透過使用 ++ 將它們加在一起來有效率地串接。

scala> def collapse(it: Iterator[Int]) = if (!it.hasNext) Iterator.empty else {
      |  var head = it.next
      |  val rest = if (head == 0) it.dropWhile(_ == 0) else it
      |  Iterator.single(head) ++ rest
      |}
def collapse(it: Iterator[Int]): Iterator[Int]

scala> def collapse(it: Iterator[Int]) = {
      |  val (zeros, rest) = it.span(_ == 0)
      |  zeros.take(1) ++ rest
      |}
def collapse(it: Iterator[Int]): Iterator[Int]

scala> collapse(Iterator(0, 0, 0, 1, 2, 3, 4)).toList
val res14: List[Int] = List(0, 1, 2, 3, 4)
scala> def collapse(it: Iterator[Int]) = if !it.hasNext then Iterator.empty else
      |  var head = it.next
      |  val rest = if head == 0 then it.dropWhile(_ == 0) else it
      |  Iterator.single(head) ++ rest
      |
def collapse(it: Iterator[Int]): Iterator[Int]

scala> def collapse(it: Iterator[Int]) =
      |  val (zeros, rest) = it.span(_ == 0)
      |  zeros.take(1) ++ rest
      |
def collapse(it: Iterator[Int]): Iterator[Int]

scala> collapse(Iterator(0, 0, 0, 1, 2, 3, 4)).toList
val res14: List[Int] = List(0, 1, 2, 3, 4)

collapse 的第二個版本中,未消耗的零會在內部進行緩衝。在第一個版本中,任何前導零都會被捨棄,而所需結果會建構為一個串聯的迭代器,它只需依序呼叫其兩個組成迭代器即可。

此頁面的貢獻者