集合(Scala 2.8 - 2.12)

迭代器

语言

迭代器不是集合,而是一种逐个访问集合元素的方法。迭代器 it 上的两个基本操作是 nexthasNext。调用 it.next() 将返回迭代器的下一个元素并推进迭代器状态。再次对同一迭代器调用 next 将产生比之前返回的元素多一个的元素。如果没有更多元素要返回,则调用 next 将抛出 NoSuchElementException。你可以使用 IteratorhasNext 方法来找出是否有更多元素要返回。

“逐步遍历”迭代器 it 返回的所有元素的最直接方法是使用 while 循环

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

Scala 中的迭代器还提供了大多数在 TraversableIterableSeq 类中找到的方法的类似方法。例如,它们提供了一个 foreach 方法,该方法对迭代器返回的每个元素执行给定的过程。使用 foreach,上面的循环可以缩写为

it foreach println

与往常一样,对于涉及 foreachmapwithFilterflatMap 的表达式,可以使用 for 表达式作为替代语法,因此打印迭代器返回的所有元素的另一种方法是

for (elem <- it) println(elem)

迭代器上的 foreach 方法和可遍历集合上的相同方法之间存在一个重要区别:当在迭代器上调用时,foreach 在完成后会将迭代器保留在末尾。因此,在同一迭代器上再次调用 next 将因 NoSuchElementException 而失败。相比之下,当在集合上调用时,foreach 会使集合中的元素数量保持不变(除非传递的函数添加或删除元素,但不建议这样做,因为它可能会导致意外结果)。

Iterator 与 Traversable 共有的其他操作具有相同的属性。例如,迭代器提供了一个 map 方法,该方法返回一个新迭代器

scala> val it = Iterator("a", "number", "of", "words")
it: Iterator[java.lang.String] = non-empty iterator
scala> it.map(_.length)
res1: Iterator[Int] = non-empty iterator
scala> res1 foreach println
1
6
2
5
scala> it.next()
java.util.NoSuchElementException: next on empty iterator

如你所见,在调用 it.map 之后,it 迭代器已推进到其末尾。

另一个示例是 dropWhile 方法,该方法可用于查找具有特定属性的迭代器的第一个元素。例如,要找到上面迭代器中第一个至少有两个字符的单词,你可以编写

scala> val it = Iterator("a", "number", "of", "words")
it: Iterator[java.lang.String] = non-empty iterator
scala> it dropWhile (_.length < 2)
res4: Iterator[java.lang.String] = non-empty iterator
scala> res4.next()
res5: java.lang.String = number

再次注意,it 已因调用 dropWhile 而改变:它现在指向列表中的第二个单词“number”。事实上,itdropWhile 返回的结果 res4 将返回完全相同的元素序列。

规避此行为的一种方法是 duplicate 底层迭代器,而不是直接在它上面调用方法。由此产生的两个迭代器将分别返回与底层迭代器 it 完全相同的元素

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

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

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

这两个迭代器独立工作:推进一个不会影响另一个,因此可以通过调用任意方法来破坏性地修改每个迭代器。这会产生同时迭代两个元素的错觉,但这种效果是通过内部缓冲实现的。与往常一样,底层迭代器 it 无法直接使用,并且必须丢弃。

总之,如果在迭代器上调用方法后不再访问该迭代器,则迭代器会表现得像集合。Scala 集合库通过一个抽象 TraversableOnce 明确表示这一点,它是 TraversableIterator 的公共超类。顾名思义,TraversableOnce 对象可以使用 foreach 遍历,但遍历后该对象的状态未指定。如果 TraversableOnce 对象实际上是 Iterator,则它将在遍历后结束,但如果它是 Traversable,它仍将像以前一样存在。 TraversableOnce 的一个常见用例是作为可以将迭代器或可遍历对象作为参数的方法的参数类型。一个示例是类 Traversable 中的追加方法 ++。它采用 TraversableOnce 参数,因此可以追加来自迭代器或可遍历集合的元素。

下面总结了对迭代器的所有操作。

Iterator 类中的操作

它是什么 它做什么
抽象方法  
it.next() 返回迭代器上的下一个元素并超越它。
it.hasNext 如果 it 可以返回另一个元素,则返回 true
变体  
it.buffered 一个缓冲迭代器,返回 it 的所有元素。
it grouped size 一个迭代器,以固定大小的序列“块”生成 it 返回的元素。
xs sliding size 一个迭代器,以表示滑动固定大小窗口的序列生成 it 返回的元素。
重复  
it.duplicate 一对迭代器,各自独立地返回 it 的所有元素。
添加  
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.toStream 收集 it 返回的元素到一个流中。
it.toSet 收集 it 返回的元素到一个集合中。
it.toMap 收集 it 返回的键/值对到一个映射中。
复制  
it copyToBuffer buf it 返回的所有元素复制到缓冲区 buf
it copyToArray(arr, s, n) it 返回的最多 n 个元素复制到数组 arr 中,从索引 s 开始。最后两个参数是可选的。
大小信息  
it.isEmpty 测试迭代器是否为空(与 hasNext 相反)。
it.nonEmpty 测试集合是否包含元素(hasNext 的别名)。
it.size it 返回的元素数量。注意:此操作后 it 将处于其末尾!
it.length it.size 相同。
it.hasDefiniteSize 如果已知 it 返回有限多个元素,则返回 true(默认情况下与 isEmpty 相同)。
元素检索索引搜索  
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) 从第 m 个元素开始,在第 n 个元素之前结束,返回从它返回的元素切片的迭代器。
it takeWhile p 只要条件 p 为真,就从 it 返回元素的迭代器。
it dropWhile p 只要条件 ptrue,就从 it 跳过元素,并返回剩余元素的迭代器。
it filter p 返回从 it 满足条件 p 的所有元素的迭代器。
it withFilter p it filter p 相同。需要这样才能在 for 表达式中使用迭代器。
it filterNot p 返回从 it 不满足条件 p 的所有元素的迭代器。
细分  
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 开始,通过用修补迭代器 jt 替换从 i 开始的 r 个元素而产生的迭代器。
比较  
it sameElements jt 测试迭代器 it 和 jt 是否按相同顺序返回相同的元素。注意:此操作后使用迭代器是未定义的,并且可能会更改。
字符串  
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,它将在需要时执行此操作。将 .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)] = Stream.from(0).zip(i)

缓冲迭代器

有时你希望一个迭代器能“向前看”,以便你检查要返回的下一个元素,而不用越过该元素。例如,考虑从返回字符串序列的迭代器中跳过前导空字符串的任务。你可能会忍不住写以下代码

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

但仔细查看此代码,很明显这是错误的:代码确实会跳过前导空字符串,但它也会推进 it 越过第一个非空字符串!

解决此问题的办法是使用缓冲迭代器。类 BufferedIteratorIterator 的子类,它提供了一个额外的方法 head。对缓冲迭代器调用 head 将返回其第一个元素,但不会推进迭代器。使用缓冲迭代器,可以按如下方式跳过空单词。

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

可以通过调用 buffered 方法将每个迭代器转换为缓冲迭代器。下面是一个示例

scala> val it = Iterator(1, 2, 3, 4)
it: Iterator[Int] = non-empty iterator
scala> val bit = it.buffered
bit: scala.collection.BufferedIterator[Int] = non-empty iterator
scala> bit.head
res10: Int = 1
scala> bit.next()
res11: Int = 1
scala> bit.next()
res12: Int = 2
scala> bit.headOption
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
     | }
collapse: (it: Iterator[Int])Iterator[Int]

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

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

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

此页面的贡献者