Статьи

Руководство по коллекциям Ruby III: перечислимый и перечислитель

collections_enum

В этой статье я расскажу о модуле 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. В следующей (и последней) части моей серии статей о коллекциях я расскажу о некоторых из моих любимых советов и приемов.