【问题标题】:How to create a "clone"-able enumerator for external iteration?如何为外部迭代创建一个“克隆”的枚举器?
【发布时间】:2020-10-19 14:07:59
【问题描述】:

我想通过next创建一个用于外部迭代的枚举器,即clone-able,以便克隆保留当前的枚举状态。

例如,假设我有一个返回枚举器的方法,该枚举器产生square numbers

def square_numbers
  return enum_for(__method__) unless block_given?

  n = d = 1
  loop do
     yield n
     d += 2
     n += d
   end
end

square_numbers.take(10)
#=> [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

我想枚举前 5 个平方数,并为每个值打印随后的 3 个平方数。 each_cons 的一些小事:

square_numbers.take(8).each_cons(4) do |a, *rest|
  printf("%2d: %2d %2d %2d\n", a, *rest)
end

输出:

 1:  4  9 16
 4:  9 16 25
 9: 16 25 36
16: 25 36 49
25: 36 49 64

但与上述不同的是,我想使用 外部迭代 使用两个嵌套循环以及 nextclone

outer_enum = square_numbers
5.times do
  i = outer_enum.next
  printf('%2d:', i)

  inner_enum = outer_enum.clone
  3.times do
    j = inner_enum.next
    printf(' %2d', j)
  end
  print("\n")
end

不幸的是,上述clone 的尝试引发了:

`initialize_copy': can't copy execution context (TypeError)

我了解 Ruby 不提供开箱即用的功能。但是我怎样才能自己实现呢?如何创建支持cloneEnumerator

假设这是实现initialize_copy 并复制nd 的两个变量值的问题,但我不知道如何或在哪里执行它。

【问题讨论】:

  • 顺便说一句,我设法使用 proc 和保存局部变量的显式绑定来使其工作。这是一个非常肮脏的 hack ......但它可能的。
  • 你为什么不发布你所做的?我希望这只是创建类的一种迂回方式,就像我建议的那样 (wiki.c2.com/?ClosuresAndObjectsAreEquivalent)。

标签: ruby clone enumerator


【解决方案1】:

无法复制 Ruby 纤程,并且 Enumerator 的 C 实现存储了一个指向纤程的指针,该纤程似乎不会以任何方式暴露给 Ruby 代码。

https://github.com/ruby/ruby/blob/752041ca11c7e08dd14b8efe063df06114a9660f/enumerator.c#L505

if (ptr0->fib) {
    /* Fibers cannot be copied */
    rb_raise(rb_eTypeError, "can't copy execution context");
}

查看 C 源代码,很明显 Enumerators 和 Fibers 以一种非常深刻的方式联系在一起。所以我怀疑有什么方法可以改变initialize_copy 的行为以允许clone

【讨论】:

  • 我理解“无法复制光纤”为“非自动”,因此您不能克隆任意枚举数。但我可以完全控制我的枚举器。也许这是一个不同的故事?
  • 嗯,注释 Fibers cannot be copy 在 Enumerator 的 C 源代码中,所以听起来很确定。
【解决方案2】:

也许您可以编写一个自己的类来满足您的要求:

class NumberSquarer
  def initialize
    @n = @d = 1
  end

  def next
    ret = @n
    @d += 2
    @n += @d
    ret
  end
end

ns1 = NumberSquarer.new
Array.new(5) { ns1.next }
# => [1, 4, 9, 16, 25]

ns2 = ns1.clone
Array.new(5) { ns2.next }
# => [36, 49, 64, 81, 100]

【讨论】:

  • 当然,重新实现Enumerator 是另一种选择。但不是我真正想要的:-)
  • 我不认为它可以用内置的。 EnumeratorProc 一样,不是可序列化的对象。我相信这是因为它们在内部持有对实时运行时内存状态的引用。通过定义自己的类,您正在设计具有明确定义的变量和无状态函数的东西,即可以复制的东西。
【解决方案3】:

免责声明:我正在回答我自己的问题


实现此目的的一种方法是对Enumerator 进行子类化。特别是现在已弃用的带有对象和方法的变体:

class ObjectEnumerator < Enumerator
  attr_reader :object, :method

  def initialize(object, method = :each)
    @object = object
    @method = method
    super
  end

  def initialize_copy(orig)
    initialize(orig.object.clone, orig.method)
  end
end

上面的 orig.object.clone 是魔法发生的地方:它克隆了我们正在遍历的对象。

此外,我们还需要这样的可克隆对象。一个简单的方法是拥有一个将状态保存为实例变量的类:(无耻地从Kache's answer复制)

class NumberSquarer
  def initialize
    @d = -1
    @n = 0
  end

  def each
    return ObjectEnumerator.new(self, __method__) unless block_given?

    loop do
      @d += 2
      @n += @d  #    had to be reordered b/c
      yield @n  # <- yield has to come last
    end
  end
end

这为我们提供了一个基本的、可克隆的枚举器:

e = NumberSquarer.new.each
#=> #<ObjectEnumerator: #<NumberSquarer:0x00007fde60915e10 @d=-1, @n=0>:each>

e.next #=> 1
e.next #=> 4

other = enum.clone
#=> #<ObjectEnumerator: #<NumberSquarer:0x00007fcf23842520 @d=3, @n=4>:each>

enum.next #=> 9
enum.next #=> 16

other.next #=> 9

【讨论】:

    【解决方案4】:

    我提供了一个不同的解决方案,它不是问题的直接答案:

    如何创建支持克隆的枚举器?

    但如果我没记错的话,克隆不可克隆 Ruby 的 Enumerator 的唯一目的是获取对枚举器中下一个对象的引用。

    在这种情况下,我们需要在下面的示例中将两个值都存储在 odd_sumsquare 中。

    我们可以将这些值存储在Array 中并返回数组而不是单个值,然后我们可以使用Enumerator.peek 来获得用于初始化新Enumerator 的数组。

    def square_numbers(starters = {})
      return enum_for(__method__, starters) unless block_given?
    
      last_odd = starters.fetch(:square_odd, [1,1])[1]
      square = starters.fetch(:square_odd, [1,1])[0]
    
      loop do
         yield [square, last_odd]
         last_odd += 2
         square += last_odd
       end
    end
    
    outer_enum = square_numbers
    5.times do
      i = outer_enum.next[0]
      printf('%2d:', i)
    
      inner_enum = square_numbers(square_odd: outer_enum.peek)
      3.times do
        j = inner_enum.next[0]
        printf(' %2d', j)
      end
      print("\n")
    end
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-07-10
      • 2018-03-07
      • 2012-11-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多