Каков «правильный» способ перебора массива в Ruby?

341

PHP, несмотря ни на что, довольно хорош в этом смысле. Нет никакой разницы между массивом и хешем (возможно, я наивен, но это, очевидно, мне подходит), и для итерации либо вы просто делаете

foreach (array/hash as $key => $value)

В Ruby есть множество способов сделать это:

array.length.times do |i|
end

array.each

array.each_index

for i in array

Хэши имеют больше смысла, так как я просто всегда использую

hash.each do |key, value|

Почему я не могу сделать это для массивов? Если я хочу запомнить только один метод, я думаю, что я могу использовать each_index(так как он делает доступным и индекс, и значение), но раздражает необходимость делать это array[index]вместо простого value.


Ах да, я забыл о array.each_with_index. Однако этот отстой, потому что он идет |value, key|и hash.eachуходит |key, value|! Разве это не безумие?

Том Леман
источник
1
Я думаю, array#each_with_indexиспользует, |value, key|потому что имя метода подразумевает порядок, тогда как порядок, используемый для hash#eachимитации hash[key] = valueсинтаксиса?
Benjineer,
3
Если вы только начинаете работать с циклами в Ruby, то проверьте, используя выбор, отклонение, сбор, внедрение и обнаружение
Мэтью Карьер

Ответы:

560

Это будет перебирать все элементы:

array = [1, 2, 3, 4, 5, 6]
array.each { |x| puts x }

Печать:

1
2
3
4
5
6

Это будет перебирать все элементы, давая вам значение и индекс:

array = ["A", "B", "C"]
array.each_with_index {|val, index| puts "#{val} => #{index}" }

Печать:

A => 0
B => 1
C => 2

Я не совсем уверен по вашему вопросу, какой вы ищете.

Роберт Гэмбл
источник
1
Не по теме я не могу найти each_with_index в ruby ​​1.9 в массиве
CantGetANick
19
@CantGetANick: класс Array включает в себя модуль Enumerable, который имеет этот метод.
xuinkrbin.
88

Я думаю, что нет единственно верного пути. Существует много разных способов итерации, и у каждого своя ниша.

  • each достаточно для многих случаев, так как меня не очень интересуют индексы.
  • each_ with _index действует как Hash # each - вы получаете значение и индекс.
  • each_index- только индексы. Я не пользуюсь этим часто. Эквивалент "length.times".
  • map это еще один способ итерации, полезный, когда вы хотите преобразовать один массив в другой.
  • select итератор, который нужно использовать, когда вы хотите выбрать подмножество
  • inject полезен для генерации сумм или продуктов или для сбора одного результата.

Может показаться, что многое нужно запомнить, но не волнуйтесь, вы можете обойтись, не зная их всех. Но когда вы начнете изучать и использовать различные методы, ваш код будет становиться все яснее и яснее, и вы будете на пути к мастерству в Ruby.

AShelly
источник
отличный ответ! Я хотел бы упомянуть обратное, как #reject и псевдонимы, как #collect.
Эмили Туй Ч
60

Я не говорю, что Array-> |value,index|и Hash-> |key,value|не безумен (см. Комментарий Горация Лоэба), но я говорю, что есть разумный способ ожидать такой договоренности.

Когда я имею дело с массивами, я сосредотачиваюсь на элементах массива (не на индексе, потому что индекс временный). Каждый метод имеет индекс, то есть каждый + индекс или | каждый, индекс | или |value,index|. Это также согласуется с тем, что индекс рассматривается как необязательный аргумент, например | значение | эквивалентно значению | index = nil | что согласуется с | значением, индексом |.

Когда я имею дело с хэш, я часто более сосредоточен на клавишах , чем значения, и я , как правило , имеет дело с ключами и значениями в указанном порядке, либо key => valueили hash[key] = value.

Если вы хотите печатать утку, то либо явно используйте определенный метод, как показал Брент Лонгборо, либо неявный метод, как показал maxhawkins.

Ruby - все о приспособлении языка к программисту, а не о программисте, приспособленном к языку. Вот почему есть так много способов. Есть так много способов думать о чем-то. В Ruby вы выбираете самый близкий, а остальная часть кода обычно получается очень аккуратно и лаконично.

Что касается первоначального вопроса: «Каков« правильный »способ перебора массива в Ruby?», Я думаю, основной способ (т.е. без мощного синтаксического сахара или объектно-ориентированной мощности) состоит в следующем:

for index in 0 ... array.size
  puts "array[#{index}] = #{array[index].inspect}"
end

Но Ruby - это все о мощном синтаксическом сахаре и объектно-ориентированной мощности, но в любом случае здесь есть эквивалент для хэшей, и ключи можно упорядочить или нет:

for key in hash.keys.sort
  puts "hash[#{key.inspect}] = #{hash[key].inspect}"
end

Итак, мой ответ: «Правильный способ перебора массива в Ruby зависит от вас (то есть программиста или команды программистов) и проекта». Лучший программист на Ruby делает лучший выбор (какой синтаксической мощности и / или какой объектно-ориентированный подход). Лучший программист на Ruby продолжает искать новые пути.


Теперь я хочу задать еще один вопрос: «Каков« правильный »способ перебора диапазона в Ruby в обратном направлении?»! (Этот вопрос, как я попал на эту страницу.)

Это приятно сделать (для форвардов):

(1..10).each{|i| puts "i=#{i}" }

но я не люблю делать (для задом наперед):

(1..10).to_a.reverse.each{|i| puts "i=#{i}" }

Ну, на самом деле я не против делать это слишком много, но когда я преподаю движение в обратном направлении, я хочу показать своим ученикам хорошую симметрию (т.е. с минимальной разницей, например, только добавление реверса или шаг -1, но без модифицируя что-нибудь еще). Вы можете сделать (для симметрии):

(a=*1..10).each{|i| puts "i=#{i}" }

и

(a=*1..10).reverse.each{|i| puts "i=#{i}" }

что я не очень люблю, но вы не можете сделать

(*1..10).each{|i| puts "i=#{i}" }
(*1..10).reverse.each{|i| puts "i=#{i}" }
#
(1..10).step(1){|i| puts "i=#{i}" }
(1..10).step(-1){|i| puts "i=#{i}" }
#
(1..10).each{|i| puts "i=#{i}" }
(10..1).each{|i| puts "i=#{i}" }   # I don't want this though.  It's dangerous

Вы могли бы в конечном итоге сделать

class Range

  def each_reverse(&block)
    self.to_a.reverse.each(&block)
  end

end

но я хочу преподавать чистый Ruby, а не объектно-ориентированные подходы (только пока). Я хотел бы повторить в обратном направлении:

  • без создания массива (рассмотрим 0..1000000000)
  • работает для любого диапазона (например, строки, а не только целые числа)
  • без использования какой-либо дополнительной объектно-ориентированной мощности (т.е. без модификации класса)

Я считаю, что это невозможно без определения predметода, что означает изменение класса Range для его использования. Если вы можете сделать это, пожалуйста, дайте мне знать, в противном случае подтверждение невозможности будет приветствоваться, хотя это будет разочаровывать. Возможно, Ruby 1.9 решает эту проблему.

(Спасибо за ваше время на чтение этого.)


источник
5
1.upto(10) do |i| puts i endи 10.downto(1) do puts i endвроде дает симметрию, которую вы хотели. Надеюсь, это поможет. Не уверен, что это будет работать для строк и тому подобное.
Сурен
(1..10) .to_a.sort {| x, y | y <=> x} .each {| i | ставит "i = # {i}"} - обратное движение медленнее.
Davidslv
Вы можете [*1..10].each{|i| puts "i=#{i}" }.
Хаулет
19

Используйте each_with_index, когда вам нужны оба.

ary.each_with_index { |val, idx| # ...
Дж Купер
источник
12

Другие ответы просто хороши, но я хотел бы отметить еще одну второстепенную вещь: массивы упорядочены, а хэши не в 1.8. (В Ruby 1.9 хэши упорядочены по порядку вставки ключей.) Поэтому до версии 1.9 не имеет смысла перебирать хэш так же, как и массивы, которые всегда имели определенный порядок. Я не знаю, каков порядок по умолчанию для ассоциативных массивов PHP (по-видимому, мой Google Fu тоже недостаточно силен, чтобы это выяснить), но я не знаю, как можно рассматривать обычные массивы PHP и ассоциативные массивы PHP для быть «одинаковым» в этом контексте, так как порядок ассоциативных массивов кажется неопределенным.

Таким образом, путь Ruby кажется мне более понятным и интуитивно понятным. :)

Pistos
источник
1
хэши и массивы - это одно и то же! массивы отображают целые числа на объекты и хэши отображают объекты на объекты. массивы - это просто особые случаи хэшей, не так ли?
Том Леман
1
Как я уже сказал, массив - это упорядоченный набор. Отображение (в общем смысле) неупорядочено. Если вы ограничите набор ключей целыми числами (например, с массивами), то случится так, что набор ключей имеет порядок. В общем отображении (хэш / ассоциативный массив) ключи могут не иметь порядка.
Pistos
@Horace: Хэши и массивы не совпадают. Если один особый случай другого, они не могут быть одинаковыми. Но что еще хуже, массивы - это не особый вид хэшей, это лишь абстрактный способ их просмотра. Как указывал выше Брент, взаимозаменяемое использование хешей и массивов может указывать на запах кода.
Зейн
9

Попытка сделать то же самое последовательно с массивами и хешами может быть просто запахом кода, но, рискуя тем, что меня заклеймят как ядовитого наполовину обезьяны-патчера, если вы ищете последовательное поведение, это бы сработало ?:

class Hash
    def each_pairwise
        self.each { | x, y |
            yield [x, y]
        }
    end
end

class Array
    def each_pairwise
        self.each_with_index { | x, y |
            yield [y, x]
        }
    end
end

["a","b","c"].each_pairwise { |x,y|
    puts "#{x} => #{y}"
}

{"a" => "Aardvark","b" => "Bogle","c" => "Catastrophe"}.each_pairwise { |x,y|
    puts "#{x} => #{y}"
}
Brent.Longborough
источник
7

Вот четыре варианта, перечисленные в вашем вопросе, упорядоченные по свободе контроля. Вы можете использовать другой в зависимости от того, что вам нужно.

  1. Просто пройдите значения:

    array.each
  2. Просто пройдите индексы:

    array.each_index
  3. Пройдите через индексы + индексную переменную:

    for i in array
  4. Счетчик цикла управления + индексная переменная:

    array.length.times do | i |
Джейк
источник
5

Я пытался создать меню (в Camping и Markaby ), используя хэш.

Каждый элемент имеет 2 элемента: метку меню и URL , поэтому хэш выглядел правильным, но URL '/' для 'Home' всегда появлялся последним (как и следовало ожидать для хэша), поэтому пункты меню отображались неправильно порядок.

Использование массива с each_sliceделает работу:

['Home', '/', 'Page two', 'two', 'Test', 'test'].each_slice(2) do|label,link|
   li {a label, :href => link}
end

Добавление дополнительных значений для каждого элемента меню (например, имя CSS- идентификатора ) просто означает увеличение значения среза. Таким образом, как хэш, но с группами, состоящими из любого количества элементов. Отлично.

Так что это просто сказать спасибо за непреднамеренный намек на решение!

Очевидно, но стоит заявить: я предлагаю проверить, делится ли длина массива на значение среза.

Дейв Эверитт
источник
4

Если вы используете перечисляемый миксин (как это делает Rails), вы можете сделать что-то похожее на приведенный фрагмент php. Просто используйте метод each_slice и сгладьте хеш.

require 'enumerator' 

['a',1,'b',2].to_a.flatten.each_slice(2) {|x,y| puts "#{x} => #{y}" }

# is equivalent to...

{'a'=>1,'b'=>2}.to_a.flatten.each_slice(2) {|x,y| puts "#{x} => #{y}" }

Требуется меньше исправлений обезьян.

Однако это вызывает проблемы, когда у вас есть рекурсивный массив или хэш со значениями массива. В ruby ​​1.9 эта проблема решается с помощью параметра для метода flatten, который указывает, насколько глубокой должна быть рекурсия.

# Ruby 1.8
[1,2,[1,2,3]].flatten
=> [1,2,1,2,3]

# Ruby 1.9
[1,2,[1,2,3]].flatten(0)
=> [1,2,[1,2,3]]

Что касается вопроса, является ли это запахом кода, я не уверен. Обычно, когда мне нужно наклониться назад, чтобы перебрать что-то, я отступаю и понимаю, что нападаю на проблему неправильно.

maxhawkins
источник
2

Правильный путь - это тот, который вам наиболее удобен и который делает то, что вы хотите. В программировании редко бывает один «правильный» способ делать вещи, чаще всего есть несколько способов выбора.

Если вас устраивает определенный способ действий, просто делайте это, если он не работает - тогда пришло время найти лучший путь.

Дариуш Г. Ягельский
источник
2

В Ruby 2.1 метод each_with_index удален. Вместо этого вы можете использовать each_index

Пример:

a = [ "a", "b", "c" ]
a.each_index {|x| print x, " -- " }

производит:

0 -- 1 -- 2 --
Амжед Шариф
источник
4
Это неправда. Как указано в комментариях к принятому ответу, причина each_with_indexне указана в документации, потому что она предоставляется Enumerableмодулем.
ihaztehcodez
0

Использование одного и того же метода для перебора как массивов, так и хэшей имеет смысл, например, для обработки вложенных структур хеш-массивов, часто возникающих в результате синтаксического анализа, чтения файлов JSON и т. Д.

Один умный способ, который еще не был упомянут, состоит в том, как это сделать в библиотеке Ruby Facets стандартных расширений библиотеки. От сюда :

class Array

  # Iterate over index and value. The intention of this
  # method is to provide polymorphism with Hash.
  #
  def each_pair #:yield:
    each_with_index {|e, i| yield(i,e) }
  end

end

Уже Hash#each_pairесть псевдоним Hash#each. Таким образом, после этого патча мы также имеем Array#each_pairи можем использовать его взаимозаменяемо для перебора как хешей, так и массивов. Это исправляет наблюдаемое безумие OP, Array#each_with_indexу которого аргументы блока обращены по сравнению с Hash#each. Пример использования:

my_array = ['Hello', 'World', '!']
my_array.each_pair { |key, value| pp "#{key}, #{value}" }

# result: 
"0, Hello"
"1, World"
"2, !"

my_hash = { '0' => 'Hello', '1' => 'World', '2' => '!' }
my_hash.each_pair { |key, value| pp "#{key}, #{value}" }

# result: 
"0, Hello"
"1, World"
"2, !"
tanius
источник