使用 Ruby 中的 to_enum 创建可枚举对象有什么优势?

15
为什么在Ruby中创建对象的代理引用时要使用to_enum方法,而不是直接使用对象?我想不到任何实际用途,试图理解这个概念以及有人可能在哪里使用它,但我看到的所有示例都似乎非常琐碎。例如,为什么要使用:
"hello".enum_for(:each_char).map {|c| c.succ }

替换为

"hello".each_char.map {|c| c.succ }

我知道这只是一个非常简单的例子,有没有人有任何现实世界的例子?

5个回答

17

大多数接受块的内置方法,如果没有提供块(例如您的示例中的String#each_char),将返回一个枚举器。对于这些方法,没有理由使用to_enum;两者具有相同的效果。

然而有一些方法不会返回一个枚举器,在这种情况下您可能需要使用to_enum

# How many elements are equal to their position in the array?
[4, 1, 2, 0].to_enum(:count).each_with_index{|elem, index| elem == index} #=> 2

举个例子,Array#product#uniq#uniq! 以前不支持块。在1.9.2中,这一点已经改变,但为了保持兼容性,没有块的形式不能返回一个Enumerator。仍然可以“手动”使用to_enum来获取一个枚举器:

require 'backports/1.9.2/array/product' # or use Ruby 1.9.2+
# to avoid generating a huge intermediary array:
e = many_moves.to_enum(:product, many_responses)
e.any? do |move, response|
  # some criteria
end 

to_enum 的主要用途是在实现自己的迭代方法时使用。通常你的第一行代码会是:

def my_each
  return to_enum :my_each unless block_given?
  # ...
end

1
如果你正在使用不返回枚举器的第三方库,这也非常有用。 - Andrew Grimm

3

假设我们想要将一组键和一组值组合成哈希表:

使用 #to_enum

def hashify(k, v)
  keys = k.to_enum(:each)
  values = v.to_enum(:each)
  hash = []
  loop do
    hash[keys.next] = values.next
    # No need to check for bounds,
    # as #next will raise a StopIteration which breaks from the loop
  end
  hash
end

Without #to_enum:

def hashify(k, v)
  hash = []
  keys.each_with_index do |key, index|
    break if index == values.length
    hash[key] = values[index]
  end
  hash
end

第一种方法更容易阅读,是不是?虽然差别不是很大,但如果我们要从三个、五个或十个数组中操作元素,想象一下会有多么困难。


1
糟糕的例子。使用 next 会影响性能。两个示例没有以相同的方式处理大小差异(一个引发异常,另一个停止执行)。 - Marc-André Lafortune

3

我认为这与内部迭代器和外部迭代器有关。当您像这样返回一个枚举器时:

p = "hello".enum_for(:each_char)

p是一个外部枚举器。外部枚举器比内部枚举器更灵活。例如,使用外部枚举器很容易比较两个集合的相等性,但使用内部枚举器则几乎不可能……但另一方面,内部枚举器更易于使用,因为它们为您定义了迭代逻辑。[来自《Ruby编程语言》第5.3章]

因此,使用外部迭代器,您可以执行以下操作:

p = "hello".enum_for(:each_char)
loop do
    puts p.next
end

1
这并不完全是对你问题的回答,但希望它是相关的。在你的第二个例子中,你调用了没有传递块的each_char。当没有传递块时,each_char返回一个Enumerator,所以你的例子实际上只是两种做同一件事情的方式。(即都结果创建了一个可枚举对象。)
irb(main):016:0> e1 = "hello".enum_for(:each_char)
=> #<Enumerator:0xe15ab8>
irb(main):017:0> e2 = "hello".each_char
=> #<Enumerator:0xe0bd38>
irb(main):018:0> e1.map { |c| c.succ }
=> ["i", "f", "m", "m", "p"]
irb(main):019:0> e2.map { |c| c.succ }
=> ["i", "f", "m", "m", "p"]

1

它非常适用于大型或无限的生成器对象。例如,以下代码将为您提供整个斐波那契数列的枚举器,从0到无穷大。

def fib_sequence
  return to_enum(:fib_sequence) unless block_given?
  yield 0
  yield 1
  x,y, = 0, 1
  loop { x,y = y,x+y; yield(y) }
end
< p > to_enum 可以让您使用常规的 yields,而无需处理 Fiber

然后,您可以根据需要对其进行切片,并且它将非常节省内存,因为不会在内存中存储任何数组:

module Slice
    def slice(range)
        return to_enum(:slice, range) unless block_given?
        start, finish = range.first, range.max + 1
        copy = self.dup
        start.times { copy.next }
        (finish-start).times { yield copy.next }
    end
end
class Enumerator
    include Slice
end

fib_sequence.slice(0..10).to_a
#=> [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
fib_sequence.slice(10..20).to_a                                                                                                                           
#=> [55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765]

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接