Мне нравятся Enumerators , много - не только для существующих типов, но и для написания своих собственных коллекций в качестве классов Enumerator. После перехода на 1.9 мне дважды приходилось создавать адаптеры API для внешних веб-сервисов, которые используют большие наборы результатов JSON или XML. Иногда я ограничивался количеством записей, которые я мог получить одновременно, а это означало, что мне нужно было сделать несколько запросов. (Получите первые 500, затем получите записи от 501 до 1000 и т. Д.)
«Старый» способ, которым я бы их обработал, состоял в том, чтобы захватить первый пакет, выполнить итерацию всего сразу с помощью .each
или .collect
и создать массив объектов Ruby одинакового размера. Если бы я не мог получить все записи в одном запросе, я бы также перебирал запросы API, добавляя их в массив каждый раз. Это означает, что все время загружается с фронта, воспринимается как медленный поиск, и я жую лот памяти: для исходных данных, для равного количества Ruby объекты, а иногда и для промежуточных операций с массивами. Это расточительно, когда я, вероятно, работаю только с одним объектом за раз.
С помощью перечислителей я могу захватить первый пакет, сохранить исходные данные как свою «авторитетную» коллекцию, обработать и выдать каждый объект Ruby , когда я войду в него . Когда я передаю последний элемент, если я знаю, что из источника нужно извлечь больше данных, я могу сделать следующий вызов API. (То есть, отложенная загрузка.) Это означает гораздо более быстрый возврат при вызове метода извлечения и намного лучшее использование памяти. Каждый объект Ruby имеет право на сборку мусора, как только я закончу с ним и перейду к следующему.
Абстрактная реализация идеи выглядит так:
class ThingyCollection < Enumerator
attr_reader :total
# Returns a new collection of thingies.
def initialize(options={})
# Make the request for the first batch
response = ThingyAPIClient.get_thingies(options)
@total = response.total # Number of ALL thingies, not just first batch
records = response.data # Some array of JSON/XML/etc. from the API
# Create a closure which serves as our enumerator code
enum = Proc.new do |yielder|
counter = 0 # Initialize our iterator
while counter < @total
# If we're at the end of this batch, get more records
if counter == records.length
more = ThingyAPIClient.get_next_thingies(counter, options)
records += more.data
end
# Return a Ruby object for the current record
yielder.yield Thingy.new(records[counter])
counter += 1
end
end
# Pass that closure to the Enumerator class
super(&enum)
end
end
Если у вас есть это, вы можете пройтись по ним, как:
thingies = ThingyCollection.new(foo: bar) # Whatever search options are relevant
puts "Our first thingy is #{thingies.next}"
puts "Our second thingy is #{thingies.next}"
thingies.rewind
thingies.each do |thingy|
do_stuff(thingy)
end
Что вы теряете? Преимущественно способность легко переходить к определенному элементу по ссылке. (Это означает, что вы также теряете «последний», сортировки и т. Д.) Просто получить .next
и пару вариантов .each
не так богато, как функциональность массива, но для моих наиболее распространенных вариантов использования это все, что мне нужно.
Да, вы можете сделать это с Ruby 1.8.7 благодаря бэкпорту. Но 1.9 намного быстрее благодаря внутреннему использованию волокон. И если бы не было 1.9, не было бы 1.8.7, поэтому я решил, что он по-прежнему считается моей любимой 1.9 функцией.