Чтобы понять главное преимущество класса перечислителя, сначала необходимо различать внутренние и внешние итераторы. При использовании внутренних итераторов итератор сам управляет итерацией. С внешними итераторами клиент (часто программист) управляет итерацией. Клиенты, использующие внешний итератор, должны продвигать обход и явно запрашивать следующий элемент у итератора. Напротив, клиент передает внутреннему итератору операцию для выполнения, и итератор применяет эту операцию к каждому элементу в коллекции.
В Ruby класс Enumerator позволяет использовать внешние итераторы. И как только вы поймете, что такое внешние итераторы, вы начнете открывать множество преимуществ. Во-первых, давайте посмотрим, как класс Enumerator упрощает внешнюю итерацию:
class Fruit
def initialize
@kinds = %w(apple orange pear banana)
end
def kinds
yield @kinds.shift
yield @kinds.shift
yield @kinds.shift
yield @kinds.shift
end
end
f = Fruit.new
enum = f.to_enum(:kinds)
enum.next
=> "apple"
f.instance_variable_get :@kinds
=> ["orange", "pear", "banana"]
enum.next
=> "orange"
f.instance_variable_get :@kinds
=> ["pear", "banana"]
enum.next
=> "pear"
f.instance_variable_get :@kinds
=> ["banana"]
enum.next
=> "banana"
f.instance_variable_get :@kinds
=> []
enum.next
StopIteration: iteration reached an end
Важно отметить, что вызов to_enum для объекта и передача символа, соответствующего методу, создаст экземпляр класса Enumerator, и в нашем примере локальная переменная enum содержит экземпляр Enumerator. Затем мы используем внешнюю итерацию для обхода созданного нами метода перечисления. Наш метод перечисления называется «виды», и обратите внимание, что мы используем метод yield, который мы обычно делаем с блоками. Здесь перечислитель будет выдавать одно значение за раз. Он делает паузу после каждого выхода. При запросе другого значения он возобновится сразу после последнего полученного значения и будет выполняться до следующего полученного значения. Когда ничего не осталось, и вы вызываете next, он вызовет исключение StopIteration.
Так в чем сила внешней итерации в Ruby? Есть несколько преимуществ, и я выделю некоторые из них. Во-первых, класс Enumerator допускает цепочку. Например, with_index определен в классе Enumerator и позволяет нам указать начальное значение для итерации при переборе объекта Enumerator:
f.instance_variable_set :@kinds, %w(apple orange pear banana)
enum.rewind
enum.with_index(1) do |name, i|
puts "#{name}: #{i}"
end
apple: 1
orange: 2
pear: 3
banana: 4
Во-вторых, он предоставляет ТОННУ полезных удобных методов из модуля Enumerable. Помните, что Enumerator — это класс, а Enumerable — это модуль, но модуль Enumerable включен в класс Enumerator, поэтому перечислители являются Enumerable:
Enumerator.ancestors
=> [Enumerator, Enumerable, Object, Kernel, BasicObject]
f.instance_variable_set :@kinds, %w(apple orange pear banana)
enum.rewind
enum.detect {|kind| kind =~ /^a/}
=> "apple"
enum
=> #<Enumerator: #<Fruit:0x007fb86c09bdf8 @kinds=["orange", "pear", "banana"]>:kinds>
И есть еще одно важное преимущество Enumerator, которое может быть не сразу понятно. Позвольте мне объяснить это с помощью демонстрации. Как вы, наверное, знаете, любой из ваших пользовательских классов можно сделать Enumerable, включив модуль Enumerable и определив метод каждого экземпляра:
class Fruit
include Enumerable
attr_accessor :kinds
def initialize
@kinds = %w(apple orange pear banana)
end
def each
@kinds.each { |kind| yield kind }
end
end
Это здорово. Теперь у нас есть масса полезных методов экземпляра Enumerable, таких как chunk
, drop_while
, flat_map
, grep
, lazy
, partition
, reduce
, take_while
и другие.
f.partition {|kind| kind =~ /^a/ }
=> [["apple"], ["orange", "pear", "banana"]]
Интересно отметить, что каждый из методов экземпляра модуля Enumerable фактически вызывает наш метод each за кулисами, чтобы получить перечисляемые элементы. Итак, если бы мы реализовали метод сокращения, это могло бы выглядеть примерно так:
module Enumerable
def reduce(acc)
each do |value|
acc = yield(acc, value)
end
acc
end
end
Обратите внимание, как он передает блок методу each, поэтому ожидается, что наш метод each вернет что-то обратно в блок.
Но посмотрите, что произойдет, если клиентский код вызовет метод each без указания блока:
f.each
LocalJumpError: no block given (yield)
Итак, теперь мы можем изменить наш метод each для использования enum_for, который будет возвращать объект Enumerator, когда блок не задан:
class Fruit
include Enumerable
attr_accessor :kinds
def initialize
@kinds = %w(apple orange pear banana)
end
def each
return enum_for(:each) unless block_given?
@kinds.each { |kind| yield kind }
end
end
f = Fruit.new
f.each
=> #<Enumerator: #<Fruit:0x007ff70aa3b548 @kinds=["apple", "orange", "pear", "banana"]>:each>
И теперь у нас есть экземпляр Enumerator, которым мы можем управлять с помощью нашего клиентского кода для последующего использования.
person
Donato
schedule
28.10.2018