Оглавление | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11

Официальный FAQ по Ruby

If you wish to report errors or suggest improvements for this FAQ, please go to our GitHub repository and open an issue or pull request.

Итераторы

Что такое итератор?

Итератор — это метод, который принимает блок или объект Proc. В исходном файле блок помещается сразу после вызова метода. Итераторы используются для создания пользовательских структур управления — особенно циклов.

Давайте рассмотрим пример, чтобы увидеть, как это работает. Итераторы часто используются для повторения одного и того же действия для каждого элемента коллекции, например так:

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

Результат:

1
2
3

Методу each массива data передается блок do ... end, и он выполняет его многократно. При каждом вызове блоку передаются последовательные элементы массива.

Вы можете определять блоки с помощью { ... } вместо do ... end.

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

Результат:

1
2
3

Этот код имеет тот же смысл, что и в предыдущем примере. Однако в некоторых случаях из-за приоритетов операций do ... end и { ... } ведут себя по-разному.

foobar a, b do ... end  # foobar является итератором.
foobar a, b { ... }     # b является итератором.

Это происходит потому, что { ... } связывается с предшествующим выражением сильнее, чем блок do ... end. Первый пример эквивалентен foobar(a, b) do ... end, а второй — foobar(a, b { ... }).

Как мне передать блок итератору?

Вы просто помещаете блок после вызова итератора. Вы также можете передать объект Proc, добавив & перед именем переменной или константы, которая ссылается на Proc.

Как блок используется в итераторе?

This section or parts of it might be out-dated or in need of confirmation.

Существует три способа выполнить блок из метода итератора: (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

Если в определении метода есть аргумент блока (перед последним формальным параметром стоит амперсанд (&)), он получит прикрепленный блок, преобразованный в объект Proc. Его можно вызвать с помощью prc.call(args).

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

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

Результат:

1
2

Proc.new (или эквивалентные вызовы proc или lambda) при использовании в определении итератора берет блок, переданный методу в качестве аргумента, и генерирует из него объект процедуры. (proc и lambda фактически являются синонимами).

[Требуется обновление: lambda ведет себя немного иначе и вызывает предупреждение tried to create Proc object without a block.]

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 это [1, 4], затем [2, 5], затем [3, 6]
end