跳至内容

Ruby 编程/参考/对象/Enumerable

来自维基教科书,开放的书籍,开放的世界

Enumerable

[编辑 | 编辑源代码]

Enumerator 在 Ruby 中以 Enumerable::Enumerator 的形式出现在 1.8.x 中,而在 1.9.x 中则以 (仅) Enumerator 的形式出现。

Enumerator 的形式

[编辑 | 编辑源代码]

Enumerator 有多种不同的使用方法

  • 作为“each”的代理
  • 作为块中值的来源
  • 作为外部迭代器

1. 作为“each”的代理

[编辑 | 编辑源代码]

这是在 ruby 1.8 中引入的 Enumerator 的第一种使用方法。它解决了以下问题:Enumerable 方法如 #map 和 #select 会在你的对象上调用 #each,但如果你想使用其他方法(如 #each_byte 或 #each_with_index)进行迭代怎么办?

Enumerator 是一个简单的代理对象,它接收对 #each 的调用并将它重定向到基础对象上的不同方法。

require 'enumerator'   # needed in ruby <= 1.8.6 only

src = "hello"
puts src.enum_for(:each_byte).map { |b| "%02x" % b }.join(" ")

对 'enum_for'(或等效地 'to_enum')的调用创建了 Enumerator 代理。它是以下内容的简写

newsrc = Enumerable::Enumerator.new(src, :each_byte)
puts newsrc.map { |b| "%02x" % b }.join(" ")

在 ruby 1.9 中,Enumerable::Enumerator已更改为Enumerator

2. 作为块中值的来源

[编辑 | 编辑源代码]

在 ruby 1.9 中,Enumerator.new 可以接收一个块,该块在调用 #each 时执行,并直接产生值。

block =  Enumerator.new {|g| g.yield 1; g.yield 2; g.yield 3}

block.each do |item|
  puts item
end

“g << 1”是“g.yield 1”的替代语法

没有使用 Fiber 或 Continuation 等高级语言功能,这种形式的 Enumerator 很容易移植到 ruby 1.8

它与创建产生值的自定义对象非常相似

block = Object.new
def block.each
  yield 1; yield 2; yield 3
end

block.each do |item|
  puts item
end

但是,它也为后面将要描述的可枚举对象的“延迟”评估奠定了基础。

3. 作为外部迭代器

[编辑 | 编辑源代码]

ruby 1.9 还允许你反转 Enumerator,使其成为值的“拉取”源,有时也称为“外部迭代”。仔细观察它与上一个例子的区别

block =  Enumerator.new {|g| g.yield 1; g.yield 2; g.yield 3}

while item = block.next
  puts item
end

控制流来回切换,第一次调用 #next 时会创建一个 Fiber,它保存调用之间的状态。因此,它的效率低于直接使用 #each 进行迭代。

当你调用 #next 且没有更多值时,会抛出一个StopIteration异常。这会被 while 循环静默捕获。StopIterationIndexError的子类,而IndexError.

又是

require 'generator'
block = Generator.new {|g| g.yield 1; g.yield 2; g.yield 3}

while block.next?
  puts block.next
end

StandardError

的子类

ruby 1.8 中最接近的等效功能是 Generator,它是使用 Continuations 实现的。

延迟评估

  Enumerator.new do |y|
    source.each do |input|     # filter INPUT
      ...
      y.yield output           # filter OUTPUT
    end
  end

[编辑 | 编辑源代码]

class Enumerator
  def defer(&blk)
    self.class.new do |y|
      each do |*input|
        blk.call(y, *input)
      end
    end
  end
end

在带有块的 Enumerator 中,要产生的目标会作为显式参数传递。这使得有可能设置方法调用的链,以便每个值从左到右传递到整个链,而不是在每一步都构建中间值数组。

res = (1..1_000_000_000).to_enum.
  defer { |out,inp| out.yield inp if inp % 2 == 0 }.   # like select
  defer { |out,inp| out.yield inp+100 }.               # like map
  take(10)
p res

基本模式是带有块的 Enumerator,它处理输入值并为每个输入值产生(零个或多个)输出值。

所以,让我们把它包装在一个便利方法中

这个新的方法 'defer' 可以用作 select 和 map 的“延迟”形式。它不会构建值数组并在最后返回该数组,而是立即产生每个值。这意味着你可以更快地获得答案,并且它可以处理巨大甚至无限长的列表。示例

虽然我们从一个包含十亿个项目的列表开始,但在最后我们只使用了生成的第一个 10 个值,因此我们在此完成后停止迭代。

你可以在 ruby 1.8 中使用相同的功能,使用facets 库。为了方便起见,它还提供了一个Denumberable 模块,其中包含常见 Enumerable 方法(如 map、select 和 reject)的延迟版本。

>> a = ["foo","bar","baz"]
=> ["foo", "bar", "baz"]
>> b = a.each_with_index
=> #<Enumerable::Enumerator:0xb7d7cadc>
>> b.each { |args| p args }
["foo", 0]
["bar", 1]
["baz", 2]
=> ["foo", "bar", "baz"]
>> 

返回 Enumerators 的方法[编辑 | 编辑源代码]从 1.8.7 开始,许多 Enumerable 方法如果未提供块,将返回 Enumerator。

src = "hello"
puts src.each_byte.map { |b| "%02x" % b }.join(" ")

这意味着通常你不需要显式调用

=> ["foo", "bar", "baz"]
>> b = a.select
=> #<Enumerable::Enumerator:0xb7d6cfb0>
>> b.each { |arg| arg < "c" }
=> ["bar", "baz"]
>> 

enum_for

本页面上的第一个示例简化为

工单 707

enum_for 在 Strictly Untyped 博客中

Generator 在 Anthony Lewis 的博客中

array = ['Superman','Batman','The Hulk']

array.each_with_index do |item,index|
  puts "#{index} -> #{item}"
 end

# will print
# 0 -> Superman
# 1 -> Batman
# 2 -> The Hulk

each_with_index

[编辑 | 编辑源代码]

each_with_index 会使用项及其索引调用其块。

range = 1 .. 10

# find the even numbers

array = range.find_all { |item| item % 2 == 0 }

# returns [2,4,6,8,10]
array = ['Superman','Batman','Catwoman','Wonder Woman']

array = array.find_all { |item| item =~ /woman/ }

# returns ['Catwoman','Wonder Woman']
find_all
华夏公益教科书