如何在Scala中复制迭代器?

时间:2011-10-17 18:34:14

标签: scala iterator

关于重复

这不是How to clone an iterator?

的副本

请不要盲目地关闭这个问题,在所谓的重复不要工作中给出的所有答案。 OP负责另一个问题,显然,答案符合HIS问题,但不符合我的问题。

并非每个类似的问题都是重复的,SE上有“扩展问题”这样的功能,唯一的方法是再次询问同一主题,以获得不同的,有效的答案。

问题

我有迭代器。我想得到它的复制(复制),然后我可以完全独立地继续原始和复制。

重要

通过反射或序列化进行复制是不行的(性能损失)。

示例

var list = List(1,2,3,4,5)
var it1 = list.iterator
it1.next()

var it2 = it1   // (*)
it2.next()

println(it1.next())

这只会引用 it1 ,因此在更改 it1 时, it2 也会发生变化,反之亦然。

上面的示例使用列表,我目前正在努力解决 HashMap 问题,但问题是一般问题 - 只是迭代器。

方法#1

如果你编辑行(*)并写:

var it2 = it1.toList.iterator

(这被建议作为链接问题中的解决方案)执行程序时抛出异常。

方法#2

“你拿着名单......”。不,我没有。我没有列表,我有迭代器。一般来说,我不知道任何关于迭代器的基础的集合,我唯一拥有的是迭代器。我必须“分叉”它。

4 个答案:

答案 0 :(得分:14)

您不能在不破坏它的情况下复制迭代器。 iterator的合同是它只能遍历一次。

您链接的问题显示了如何获取两个副本以换取您销毁的副本。您无法继续使用原件,但现在可以独立向前运行两个新副本。

答案 1 :(得分:9)

创建一个可以复制而不会破坏它的List迭代器非常容易:这基本上是从iterator源复制的List方法的定义{{1添加了方法:

fork

使用:

class ForkableIterator[A] (list: List[A]) extends Iterator[A] {
    var these = list
    def hasNext: Boolean = !these.isEmpty
    def next: A = 
      if (hasNext) {
        val result = these.head; these = these.tail; result
      } else Iterator.empty.next
    def fork = new ForkableIterator(these)
}

我看过为scala> val it = new ForkableIterator(List(1,2,3,4,5,6)) it: ForkableIterator[Int] = non-empty iterator scala> it.next res72: Int = 1 scala> val it2 = it.fork it2: ForkableIterator[Int] = non-empty iterator scala> it2.next res73: Int = 2 scala> it2.next res74: Int = 3 scala> it.next res75: Int = 2 执行此操作但看起来更复杂(部分原因是根据集合大小有不同的地图实现)。因此,最好在HashMap上使用上述实现。

答案 2 :(得分:6)

正如雷克斯所说,在不破坏它的情况下制作迭代器的副本是不可能的。也就是说,duplicate会出现什么问题?

var list = List(1,2,3,4,5)
var it1 = list.iterator
it1.next()

val (it1a, it1b) = it1.duplicate
it1 = it1a
var it2 = it1b
it2.next()

println(it1.next())

答案 3 :(得分:0)

我认为这是一个非常好的问题,很遗憾,许多人不了解问题的价值。在大数据时代,很多情况下我们都有一个流,而不是无法收集或无法放入内存的已分配数据列表。而且从一开始就重复进行它也是昂贵的。如果我们需要对数据进行两次(或更多次)单独的计算,该怎么办?例如,我们可能需要使用已经编写的函数来计算最小值,最大值,总和,md5等,而在不同线程中只有一次通过。

一般的解决方案是使用Akka-Stream。这样就可以了。 但是,使用Iterator可能吗?这是Java / Scala中表示此类流数据源的最简单方法吗? 答案是肯定的,尽管我们“不能继续进行原著并完全独立地进行复制”,这意味着我们必须同步每个使用者线程的消耗速度。 (Akka-Stream通过使用背压和一些中间缓冲区来实现此目的。)

所以这是我的简单解决方案:使用Phaser。有了它,我们可以使Iterator包装器遍历一遍源。该对象将在每个使用者线程中用作简单的Iterator。使用它,您将提前知道消耗线程的数量。同样,每个使用者线程都必须耗尽源,直到结束为止,以免造成所有问题(例如使用flush()方法)。

import java.util.concurrent.Phaser
import java.util.concurrent.atomic.AtomicBoolean

// it0 - input source iterator
// num - exact number of consuming threads. We have to know it in advance.
case class ForkableIterator[+A]( it0: Iterator[A], num: Int ) extends Phaser(num) with Iterator[A] {

  val it = it0.flatMap( Stream.fill(num)(_) )  // serial replicator

  private var hasNext0 = new AtomicBoolean( it0.hasNext )
  override def hasNext: Boolean = hasNext0.get()

  override def next(): A = {
    arriveAndAwaitAdvance()
    val next = it.synchronized {
      val next = it.next()
      if (hasNext0.get) hasNext0.set(it.hasNext)
      next
    }
    arriveAndAwaitAdvance() // otherwise the tasks locks at the end the last data element
    next
  }

  // In case that a consumer gives up to read before the end of its source data stream occurs
  // it HAVE to drain the last to avoid block others. (Note: Phaser has no "unregister" method?).
  // Calling it may be avoided if all consumers read exactly the same amount of data,
  // e.g. until the very end of it.
  def flush(): Unit = while (hasNext) next()
}

PS我成功地将这个“ ForkableIterator”与Spark配合使用,以对较长的源数据流执行几个独立的聚合。在这种情况下,我不必手动创建线程。您也可以使用Scala Futures / Monix Tasks等。

PSPS现在,我重新检查了JDK Phaser规范,发现它实际上具有名为“ unknownAndDeregister()”的“取消注册”方法。因此,如果使用者完成,请使用它代替flush()。