Официальный 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