集合

迭代器

语言

迭代器不是集合,而是一种逐个访问集合元素的方法。迭代器 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 表达式可以用作涉及 foreachmapwithFilterflatMap 的表达式的备用语法,因此打印迭代器返回的所有元素的另一种方法是

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) it 中的每个元素应用部分函数 f(该元素已定义)并收集结果获得的迭代器。
转换  
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) 只要条件 ptrue,就从 it 跳过元素,并返回其余元素的迭代器。
it.filter(p) 返回满足条件 pit 中的所有元素的迭代器。
it.withFilter(p) it filter p 相同。需要它以便可以在 for 表达式中使用迭代器。
it.filterNot(p) 返回不满足条件 pit 中的所有元素的迭代器。
it.distinct 返回不包含重复项的 it 中的元素的迭代器。
细分  
it.partition(p) it 拆分为两个迭代器的对:一个返回满足谓词 pit 中的所有元素,另一个返回不满足 it 中的所有元素。
it.span(p) it 拆分为两个迭代器的对:一个返回满足谓词 pit 的前缀的所有元素,另一个返回 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 开始,用修补迭代器 jt 替换从 i 开始的 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 中使用 printlnprintln 用于演示惰性,因为通常在纯函数中看不到惰性。

尽管懒惰通常不可见,但它仍然很有价值,因为它可以防止不必要的计算发生,并且可以处理无限序列,如下所示

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 的第二个版本中,未消耗的零在内部进行缓冲。在第一个版本中,任何前导零都将被丢弃,并且将所需结果构造为一个连接的迭代器,它只是依次调用其两个组成迭代器。

此页面的贡献者