内容 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11

官方 Ruby FAQ

如果您希望报告错误或对本 FAQ 提出改进建议,请访问我们的 GitHub 仓库,并提出 issue 或 pull request。

迭代器

什么是迭代器?

迭代器是一种接受块(block)或 Proc 对象的方法。在源文件中,块紧跟在方法调用之后。迭代器用于生成用户定义的控制结构——特别是循环。

让我们看一个例子,了解它是如何工作的。迭代器经常用于对集合中的每个元素重复相同的操作,如下所示:

data = [1, 2, 3]
data.each do |i|
  puts i
end

产生

1
2
3

数组 dataeach 方法接收 do ... end 块,并反复执行它。每次调用时,块都会接收数组的连续元素。

您可以使用 { ... } 来定义块,代替 do ... end

data = [1, 2, 3]
data.each { |i|
  puts i
}

产生

1
2
3

此代码与上一个示例的含义相同。然而,在某些情况下,优先级问题会导致 do ... end{ ... } 的作用不同。

foobar a, b do ... end  # foobar is the iterator.
foobar a, b { ... }     # b is the iterator.

这是因为 { ... }do ... end 块与前面的表达式结合得更紧密。第一个示例等同于 foobar(a, b) do ... end,而第二个示例是 foobar(a, b { ... })

如何将块传递给迭代器?

您只需将块放在迭代器调用之后。您也可以通过在指向 Proc 的变量或常量名称前添加 & 来传递 Proc 对象。

块在迭代器中是如何使用的?

本节或部分内容可能已过时或需要确认。

有三种方法可以从迭代器方法执行块:(1)使用 yield 控制结构;(2)调用 Proc 参数(由块创建)并使用 call;(3)使用 Proc.new 后跟一个调用。

yield 语句调用块,可选地将一个或多个参数传递给它。

def my_iterator
  yield 1, 2
end

my_iterator {|a, b| puts a, b }

产生

1
2

如果方法定义有一个块参数(最后一个形式参数前面有一个 ampersand ( & )),它将接收附加的块,并将其转换为 Proc 对象。这可以使用 prc.call(args) 调用。

def my_iterator(&b)
  b.call(1, 2)
end

my_iterator {|a, b| puts a, b }

产生

1
2

在迭代器定义中使用 Proc.new(或等效的 proclambda 调用)时,它会将传递给方法的块作为其参数,并从中生成一个过程对象。(proclambda 基本上是同义词。)

[需要更新:lambda 的行为略有不同,并且会产生警告 tried to create Proc object without a block(尝试在没有块的情况下创建 Proc 对象)。]

def my_iterator
  Proc.new.call(3, 4)
  proc.call(5, 6)
  lambda.call(7, 8)
end

my_iterator {|a, b| puts a, b }

产生

3
4
5
6
7
8

也许令人惊讶的是,Proc.new 及其同类方法在任何意义上都不会消耗附加到该方法的块——每次调用 Proc.new 都会使用同一个块生成一个新的过程对象。

您可以通过调用 block_given? 来判断方法是否关联了块。

不带块的 Proc.new 有什么作用?

不带块的 Proc.new 无法生成过程对象,并会发生错误。然而,在方法定义中,不带块的 Proc.new 暗示在调用方法时存在一个块,因此不会发生错误。

如何并行运行迭代器?

这里采纳了 Matz 在 [ruby-talk:5252] 中提出的一个使用线程的解决方案。

require "thread"

def combine(*iterators)
  queues = []
  threads = []

  iterators.each do |it|
    queue = SizedQueue.new(1)
    th = Thread.new(it, queue) do |i, q|
           send(i) {|x| q << x }
         end
    queues  << queue
    threads << th
  end

  loop do
    ary = []
    queues.each {|q| ary << q.pop }
    yield ary

    iterators.size.times do |i|
      return if !threads[i].status && queues[i].empty?
    end
  end
end

def it1
  yield 1; yield 2; yield 3
end

def it2
  yield 4; yield 5; yield 6
end

combine(:it1, :it2) do |x|
  # x is [1, 4], then [2, 5], then [3, 6]
end