В этой статье я расскажу о модуле Enumerable и классе Enumerator. Чтобы максимально использовать коллекции Ruby, вам необходимо понять, как они работают и что они вам дают. В частности, Enumerable сам по себе вносит большой вклад в краткость и гибкость Ruby. На самом деле, многие новые Rubyists используют его под капотом, даже не подозревая об этом.
перечислимый
Enumerable возможен в Ruby благодаря понятию «mixins». В большинстве языков совместное использование кода возможно только через наследование, а наследование обычно ограничивается одним родительским классом. В дополнение к классам, в Ruby есть модули, которые являются контейнерами для методов и констант. Если вы хотите написать код один раз и поделиться им между несколькими несвязанными классами, вы можете поместить его в модули, которые будут «смешаны» с вашими классами. Обратите внимание, что это отличается от совместного использования интерфейса, как в Java, где используются только сигнатуры методов.
Можно узнать, какие классы используют Enumerable с Object#included_modules
.
>> Array.included_modules => [Enumerable, PP::ObjectMixin, Kernel] >> String.included_modules => [Comparable, PP::ObjectMixin, Kernel]
Чтобы класс использовал Enumerable, он должен определить метод #each
. Этот метод должен передавать каждый элемент в коллекции в блок. Помните класс Colors, который мы сделали ранее? Давайте добавим Enumerable к нему, чтобы придать ему магические итерационные способности.
>> class Colors >> include Enumerable >> def each >> yield "red" >> yield "green" >> yield "blue" >> end >> end >> c = Colors.new >> c.map { |i| i.reverse } => ["der", "neerg", "eulb"]
Можно увидеть, какие все методы предоставляет Enumerable, проверив выходные #instance_methods
метода #instance_methods
.
>> Enumerable.instance_methods.sort => [:all?, :any?, :chunk, :collect, :collect_concat, :count, :cycle, :detect, :drop, :drop_while, :each_cons, :each_entry, :each_slice, :each_with_index, :each_with_object, :entries, :find, :find_all, :find_index, :first, :flat_map, :grep, :group_by, :include?, :inject, :map, :max, :max_by, :member?, :min, :min_by, :minmax, :minmax_by, :none?, :one?, :partition, :reduce, :reject, :reverse_each, :select, :slice_before, :sort, :sort_by, :take, :take_while, :to_a, :zip]
Поиск
Enumerable предоставляет несколько функций для фильтрации ваших коллекций. Те из них, которые вы, вероятно, увидите, часто являются «действующим» семейством методов.
>> [1,2,3,4,5].select { |i| i > 3 } => [4,5] >> [1,2,3,4,5].detect { |i| i > 3 } => 4 >> [1,2,3,4,5].reject { |i| i > 3 } => [1,2,3]
#find_all
и #find
выполняют одинаковые операции.
Enumerable#grep
предоставляет возможность выполнять общий поиск. Это ярлык для #select
и оператора ===
.
Threequals ( ===
) — странный, но довольно полезный оператор. Он используется не для установления равенства в абсолютном смысле, а для более общего.
>> (1..3) === 2 => true >> (1..3) === 4 => false >> ('a'..'c') === 'b' => true >> /el/ === "Hello World" => true >> Object === Array => true
При использовании ===
более общий объект (например, Range, Regexp) располагается слева от оператора, а конкретный объект — справа. Это не работает наоборот, потому что три четверти обычно перезаписываются в одну сторону. Range#===
знает, что делать с Fixnums, но Fixnum#===
не знает, как обрабатывать диапазоны.
>> 2 === (1..3) => false
Если вы хотите сделать общий поиск с помощью select
, вы можете использовать три четверки.
>> [:hello_world, "Jello World", 3].select { |i| /.ello..orld/ === i } => [:hello_world, "Jello World"]
Enumerable#grep
— это оболочка для этого.
>> [6, 14, 28, 47, 12].grep(5..15) => [6, 14, 12] >> [0.3, "three", :three, "thirty-three"].grep /three/ => ["three", :three, "thirty-three"]
Посмотрите, что произойдет, если вы попытаетесь #map
операцию #map
в гетерогенном массиве.
>> ['a', 1, 2, 'b'].map(&:upcase) => NoMethodError: undefined method `upcase' for 1:Fixnum
Мы можем использовать #grep
чтобы отфильтровать коллекцию для элементов, которые будут принимать наш метод.
>> ['a', 1, 2, 'b'].grep(String, &:upcase) => ["A", "B"]
Сортировка
Допустим, у вас есть массив чисел, представленных как целые числа и строки. Если вы попытаетесь использовать #sort
, произойдет сбой.
>> [1, "5", 2, "7", "3"].sort ArgumentError: comparison of Fixnum with String failed
Поскольку строки не обязательно являются числами с кавычками вокруг них, Ruby не пытается неявно преобразовать строки в подходящие числа перед попыткой сортировки. Мы можем исправить это, рассказав, как сортировать элементы.
>> [1, "5", 2, "7", "3"].sort_by { |a| a.to_i } => [1, 2, "3", "5", "7"]
Вы также можете использовать немного более короткую версию.
>> [1, "5", 2, "7", "3"].sort_by(&:to_i) => [1, 2, "3", "5", "7"]
А что если вы сортируете что-то, что не является числом или не может быть легко преобразовано в одно? Метод #sort
не волшебный. Он проверяет результат метода оператора комбинированного сравнения ( #<=>
).
Метод #<=>
работает так:
>> 1 <=> 2 => -1 >> 2 <=> 1 => 1 >> 1 <=> 1 => 0
Если вы хотите сделать сортируемый произвольный класс, вам просто нужно определить его. Сделайте новый класс и посмотрите, что произойдет, когда вы попытаетесь отсортировать.
>> class McGuffin >> attr_accessor :absurdity >> def initialize(a) >> @absurdity = a >> end >> end >> m1 = McGuffin.new(0.1) >> m2 = McGuffin.new(2.3) >> [m1, m2].sort => ArgumentError: comparison of McGuffin with McGuffin failed
Он выдает ошибку, потому что Ruby не знает, как сравнивать членов этого нового класса. Это легко исправить, определив #<=>
.
>> class McGuffin >> def <=>(other) >> @absurdity <=> other.absurdity >> end >> end >> [m1, m2].sort => [#>McGuffin:0x00000000de0238 @absurdity=0.1>, #>McGuffin:0x00000000de50f8 @absurdity=2.3>]
Обратите внимание, что #<=>
не заменяет другие операторы сравнения. Вам нужно будет определить их отдельно, если вы хотите их.
>> m1 > m2 => NoMethodError: undefined method `>'...
Любые? и все?
Enumerable#any?
возвращает true, если его блок равен true для любого элемента в коллекции. Enumerable#all?
возвращает true, если его блок равен true для каждого элемента.
>> [2,4,6,8].all?(&:even?) => true >> [2,4,5,8].any? { |i| i % 2 == 0 } => true >> [2,4,5,8].all? { |i| i % 2 == 0 } => false
Кому нужен Excel?
Enumerable#reduce
принимает коллекцию и сокращает ее до одного элемента. Он применяет операцию к каждому элементу, поддерживая постоянный «итог».
Например, #reduce
может использоваться для суммирования коллекции.
>> [1,2,3].reduce(:+) => 6
Ruby не поставляется с факториальным методом. Тем не менее, благодаря #reduce
легко собрать красивый хак.
>> class Integer >> def factorial >> (1..self).reduce(:*) || 1 >> end >> end >> 6.factorial => 720 >> 0.factorial => 1
Enumerable#reduce
как известно, трудно понять. До сих пор я #reduce
все просто, позволяя #reduce
работать вне поля зрения. Но теперь давайте сделаем это открыто.
>> [1,2,3].reduce(0) { |sum, element| sum + element } => 6
Воу, воу, воу. Что это за вещи, которые я только что добавил? Что означает аргумент, переданный #reduce
?
Сравните эти три # #reduce
вызовов.
[1,2,3].reduce do |accumulator, current| puts "Accumulator: #{accumulator}, Current: #{current}" accumulator + current end Accumulator: 1, Current: 2 Accumulator: 3, Current: 3 => 6 [1,2,3].reduce(0) do |accumulator, current| puts "Accumulator: #{accumulator}, Current: #{current}" accumulator + current end Accumulator: 0, Current: 1 Accumulator: 1, Current: 2 Accumulator: 3, Current: 3 => 6 [1,2,3].reduce(1) do |accumulator, current| puts "Accumulator: #{accumulator}, Current: #{current}" accumulator + current end Accumulator: 1, Current: 1 Accumulator: 2, Current: 2 Accumulator: 4, Current: 3 => 7
Я думаю, что наиболее запутанным в этом случае является разница между передачей 0 и вообще ничего.
-
#reduce
— Текущий начинается как второй элемент. Аккумулятор начинается как первый элемент. -
#reduce(x)
— Текущий начинается как первый элемент. Аккумулятор начинается как х.
Как это даже полезно? Пример, который использует ruby-doc.org, — это найти самое длинное слово в коллекции.
>> words = %w{cool bobsled Jamaican} >> longest = words.reduce do |memo, word| >> memo.length > word.length ? memo : word >> end => "Jamaican"
Первоначально #reduce
был известен как #inject
, и вы можете использовать любой из них в современном Ruby. Тем не менее, я лично предпочитаю #reduce
потому что меня #reduce
идея «впрыскивать» коллекцию в один элемент.
Бесконечная итерация
Часто бывает полезно перебирать коллекцию произвольное количество раз или даже бесконечно. Наивным способом сделать это было бы следить за индексом счетчика и сбрасывать его каждый раз, когда он достигает размера коллекции — 1 (когда коллекции индексируются с нулями, как в Ruby).
Лучшее решение — использовать инкремент и mod ( %
) размер коллекции для получения каждого индекса. Допустим, у вас есть 3 идентификатора продукта, и вам нужно выполнить 10 шагов итерации.
>> arr = ["first", "middle", "last"] >> 10.times { |i| puts arr[i % arr.size] } first middle last first middle last first middle last first
Ruby предоставляет немного более чистый способ сделать это с помощью Enumerable#cycle
.
>> arr.cycle(2) { |i| puts i } first middle last first middle last
Передача аргумента в цикл будет полностью повторять эту коллекцию много раз. Если аргумент не передан, он повторяется бесконечно (производя бесконечный цикл).
Есть несколько проблем с ездой на велосипеде таким образом:
- Аргумент к
#cycle
указывает количество циклов, а не количество элементов для циклического#cycle
- Если вы хотите выполнять бесконечный цикл, вся соответствующая логика должна идти внутри
#cycle
, потому что код никогда не покинет
К счастью, обе эти проблемы могут быть решены с помощью объекта Enumerator.
счетчик
Если #cycle
не передан блок, он вернет Enumerator.
>> cycle_enum = arr.cycle => #>Enumerator: ["first", "middle", "last"]:cycle>
Теперь Enumerator#next
можно использовать для получения следующего элемента столько раз, сколько необходимо.
>> cycle_enum.next => "first" >> cycle_enum.next => "middle" >> cycle_enum.next => "last" >> cycle_enum.next => "first"
Это работает, потому что Enumerator специально #cycle
из #cycle
. Посмотрите, что происходит, когда используется обычный #each
Enumerator.
>> each_enum = arr.each >> each_enum.next => "first" >> each_enum.next => "middle" >> each_enum.next => "last" >> each_enum.next => StopIteration: iteration reached an end
Примечание: вы не можете просто использовать #next
в #cycle
и #each
.
>> arr.cycle.next => "first" >> arr.cycle.next => "first" >> arr.cycle.next => "first"
Это связано с тем, что методы итератора каждый раз возвращают новый перечислитель.
>> arr.cycle.object_id == arr.cycle.object_id => false
Хороший способ описания объектов Enumerator состоит в том, что они содержат информацию о том, как выполнять итерацию по коллекции. Например, перечислитель #cycle
знает, как перебирать коллекцию один или несколько раз, а перечислитель #reverse_each
знает, как перебирать коллекцию в обратном направлении.
Чем это полезно?
Ну, допустим, вы хотите перебрать коллекцию задом наперед. Вы бы просто использовали #reverse_cycle
, верно?
>> [:first, :middle, :last].reverse_cycle(2) { |i| puts i } NoMethodError: undefined method `reverse_cycle'...
Дерьмо! В Enumerable нет #reverse_cycle
! Мы сказали Боссе Леди, что к полудню будем ездить на велосипеде назад. И с экономикой и все…
Но ждать. Возможно, не вся надежда потеряна. Как насчет того, чтобы взять #reverse_each
… и затем вызвать #cycle
?
>> [:first, :middle, :last].reverse_each.cycle(2) { |i| puts i } last middle first last middle first
Цепочка: это то, что вы можете сделать с Enumerator. Хотите перебрать коллекцию задом наперед и поместить результаты в массив? Просто добавьте #map
в цепочку:
>> [:first, :middle, :last].reverse_each.cycle(2).map { |i| i } => [:last, :middle, :first, :last, :middle, :first]
Вывод
Это охватывает Enumerator и Enumerable. В следующей (и последней) части моей серии статей о коллекциях я расскажу о некоторых из моих любимых советов и приемов.