Qual é a maneira "certa" de iterar através de uma matriz em Ruby?

341

O PHP, apesar de todas as suas verrugas, é muito bom nesse sentido. Não há diferença entre uma matriz e um hash (talvez eu seja ingênuo, mas isso parece obviamente correto para mim), e para iterar através de qualquer um

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

Em Ruby, existem várias maneiras de fazer esse tipo de coisa:

array.length.times do |i|
end

array.each

array.each_index

for i in array

Hashes fazem mais sentido, já que eu sempre uso

hash.each do |key, value|

Por que não posso fazer isso para matrizes? Se eu quiser me lembrar de apenas um método, acho que posso usar each_index(já que ele disponibiliza o índice e o valor), mas é irritante ter que fazer em array[index]vez de apenas value.


Ah, claro, eu esqueci array.each_with_index. No entanto, este é uma merda, porque vai |value, key|e hash.eachvai |key, value|! Isso não é insano?

Tom Lehman
fonte
11
Eu acho que array#each_with_indexusa |value, key|porque o nome do método implica a ordem, enquanto a ordem usada para hash#eachimita a hash[key] = valuesintaxe?
Benjineer
3
Se você está apenas começando com loops em Ruby, então confira usando select, rejeitar, coleta, injetar e detectar
Matthew Carriere

Respostas:

560

Isso irá percorrer todos os elementos:

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

Impressões:

1
2
3
4
5
6

Isso irá percorrer todos os elementos, fornecendo o valor e o índice:

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

Impressões:

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

Não tenho muita certeza da sua pergunta, qual você está procurando.

Robert Gamble
fonte
11
Off-topic I cant encontrar each_with_index no Ruby 1.9 em ordem
CantGetANick
19
@CantGetANick: A classe Array inclui o módulo Enumerable que possui esse método.
xuinkrbin.
88

Eu acho que não há um caminho certo . Existem várias maneiras diferentes de iterar, e cada uma tem seu próprio nicho.

  • each é suficiente para muitos usos, já que muitas vezes não me importo com os índices.
  • each_ with _index age como Hash # each - você obtém o valor e o índice.
  • each_index- apenas os índices. Eu não uso este frequentemente. Equivalente a "comprimento x vezes".
  • map é outra maneira de iterar, útil quando você deseja transformar uma matriz em outra.
  • select é o iterador a ser usado quando você deseja escolher um subconjunto.
  • inject é útil para gerar somas ou produtos ou coletar um único resultado.

Pode parecer muito para lembrar, mas não se preocupe, você pode sobreviver sem conhecer todos eles. Mas quando você começar a aprender e usar os diferentes métodos, seu código se tornará mais limpo e claro, e você estará no seu caminho para o domínio do Ruby.

AShelly
fonte
Ótima resposta! Eu gostaria de mencionar o contrário, como #reject e aliases como #collect.
Emily Tui Ch
60

Não estou dizendo que Array-> |value,index|e Hash-> |key,value|não é insano (veja o comentário de Horace Loeb), mas estou dizendo que existe uma maneira sensata de esperar esse arranjo.

Quando estou lidando com matrizes, estou focado nos elementos da matriz (não no índice porque o índice é transitório). O método é cada um com índice, ou seja, cada índice + ou | cada índice |, ou |value,index|. Isso também é consistente com o índice sendo exibido como um argumento opcional, por exemplo | valor | é equivalente a | value, index = nil | que é consistente com | valor, índice |.

Quando estou lidando com hashes, geralmente estou mais focado nas chaves do que nos valores, e geralmente estou lidando com chaves e valores nessa ordem, seja key => valueou hash[key] = value.

Se você deseja digitar patos, use explicitamente um método definido como Brent Longborough mostrou ou um método implícito como maxhawkins mostrou.

Ruby é tudo sobre como acomodar o idioma para se adequar ao programador, não sobre o programador para se adequar ao idioma. É por isso que existem tantas maneiras. Existem muitas maneiras de pensar em algo. No Ruby, você escolhe o mais próximo e o restante do código geralmente cai de maneira extremamente clara e concisa.

Quanto à pergunta original, "Qual é a maneira" correta "de iterar através de uma matriz em Ruby?", Bem, acho que a maneira principal (ou seja, sem açúcar sintático poderoso ou poder orientado a objetos) é:

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

Mas Ruby é tudo sobre açúcar sintático poderoso e poder orientado a objetos, mas de qualquer maneira aqui é o equivalente a hashes, e as chaves podem ser ordenadas ou não:

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

Então, minha resposta é: "A maneira" correta "de iterar através de uma matriz no Ruby depende de você (ou seja, do programador ou da equipe de programação) e do projeto". O melhor programador Ruby faz a melhor escolha (de qual poder sintático e / ou qual abordagem orientada a objetos). O melhor programador de Ruby continua procurando outras maneiras.


Agora, quero fazer outra pergunta: "Qual é a maneira" correta "de percorrer um Range in Ruby de trás para frente?"! (Esta questão é como cheguei a esta página.)

É bom fazer (para a frente):

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

mas eu não gosto de fazer (para trás):

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

Bem, eu realmente não me importo muito de fazer isso, mas quando estou ensinando ao contrário, quero mostrar aos meus alunos uma simetria agradável (ou seja, com diferença mínima, por exemplo, apenas adicionando um reverso ou um passo -1, mas sem modificando qualquer outra coisa). Você pode fazer (por simetria):

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

e

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

que eu não gosto muito, mas você não pode fazer

(*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

Você poderia finalmente fazer

class Range

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

end

mas eu quero ensinar Ruby puro em vez de abordagens orientadas a objeto (ainda). Gostaria de iterar para trás:

  • sem criar uma matriz (considere 0..1000000000)
  • trabalhando para qualquer intervalo (por exemplo, seqüências de caracteres, não apenas números inteiros)
  • sem usar nenhum poder extra orientado a objetos (ou seja, nenhuma modificação de classe)

Eu acredito que isso é impossível sem definir um predmétodo, o que significa modificar a classe Range para usá-lo. Se você puder fazer isso, entre em contato, caso contrário, a confirmação da impossibilidade seria apreciada, embora isso fosse decepcionante. Talvez o Ruby 1.9 resolva isso.

(Obrigado pelo seu tempo lendo isso.)


fonte
5
1.upto(10) do |i| puts i ende 10.downto(1) do puts i endmeio que dá a simetria que você queria. Espero que ajude. Não tenho certeza se funcionaria para seqüências de caracteres e tal.
Suren
(1..10) .to_a.sort {| x, y | y <=> x} .each {| i | coloca "i = # {i}"} - o inverso é mais lento.
Davidslv
Você pode [*1..10].each{|i| puts "i=#{i}" }.
Hauleth
19

Use each_with_index quando precisar de ambos.

ary.each_with_index { |val, idx| # ...
J Cooper
fonte
12

As outras respostas são ótimas, mas eu queria destacar outra coisa periférica: as matrizes são ordenadas, enquanto que os Hashes não estão no 1.8. (No Ruby 1.9, os Hashes são ordenados por ordem de inserção de chaves.) Portanto, não faria sentido antes do 1.9 iterar sobre um Hash da mesma maneira / sequência que Arrays, que sempre tiveram uma ordem definida. Eu não sei qual é a ordem padrão para matrizes associativas PHP (aparentemente meu google fu também não é forte o suficiente para descobrir isso), mas não sei como você pode considerar matrizes PHP regulares e matrizes associativas PHP para ser "o mesmo" neste contexto, uma vez que a ordem das matrizes associativas parece indefinida.

Como tal, o modo Ruby parece mais claro e intuitivo para mim. :)

Pistos
fonte
11
hashes e matrizes são a mesma coisa! matrizes mapeiam números inteiros para objetos e hashes mapeiam objetos para objetos. matrizes são apenas casos especiais de hashes, não?
Tom Lehman
11
Como eu disse, uma matriz é um conjunto ordenado. Um mapeamento (no sentido genérico) não é ordenado. Se você restringir o conjunto de chaves a números inteiros (como com matrizes), acontece que o conjunto de chaves tem um pedido. Em um mapeamento genérico (Hash / Associative Array), as chaves podem não ter um pedido.
Pistos
@ Horace: Hashes e matrizes não são os mesmos. Se um é um caso especial do outro, eles não podem ser os mesmos. Pior ainda, matrizes não são um tipo especial de hashes, é apenas uma maneira abstrata de visualizá-las. Como Brent acima apontou, o uso de hashes e matrizes de forma intercambiável pode indicar um cheiro de código.
Zane
9

Tentar fazer a mesma coisa de maneira consistente com arrays e hashes pode ser apenas um cheiro de código, mas, com o risco de eu ser rotulado como um meio-patador de macaco maciço, se você estiver procurando por um comportamento consistente, isso faria o truque? ?:

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
fonte
7

Aqui estão as quatro opções listadas na sua pergunta, organizadas por liberdade de controle. Você pode querer usar um diferente, dependendo do que você precisa.

  1. Basta passar por valores:

    array.each
  2. Basta acessar os índices:

    array.each_index
  3. Percorra índices + variável de índice:

    for i in array
  4. Contagem do loop de controle + variável de índice:

    array.length.times do | i |
Jake
fonte
5

Eu estava tentando criar um menu (em Camping e Markaby ) usando um hash.

Cada item possui 2 elementos: um rótulo de menu e um URL ; portanto, um hash parecia certo, mas o URL '/' para 'Home' sempre aparecia por último (como seria de esperar para um hash); portanto, os itens de menu apareciam incorretos ordem.

O uso de uma matriz com each_slicefaz o trabalho:

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

Adicionar valores extras para cada item de menu (por exemplo, como um nome de ID CSS ) significa apenas aumentar o valor da fatia. Portanto, como um hash, mas com grupos que consistem em qualquer número de itens. Perfeito.

Portanto, isso é apenas para agradecer por sugerir inadvertidamente uma solução!

Óbvio, mas vale a pena afirmar: sugiro verificar se o comprimento da matriz é divisível pelo valor da fatia.

Dave Everitt
fonte
4

Se você usar o mixin enumerável (como o Rails), poderá fazer algo semelhante ao snippet php listado. Basta usar o método each_slice e achatar o hash.

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}" }

É necessário menos patch para macacos.

No entanto, isso causa problemas quando você tem uma matriz recursiva ou um hash com valores de matriz. No ruby ​​1.9, esse problema é resolvido com um parâmetro para o método flatten que especifica a profundidade da recuperação.

# 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]]

Quanto à questão de saber se é um cheiro de código, não tenho certeza. Geralmente, quando tenho que me inclinar para trás para repetir algo, dou um passo atrás e percebo que estou atacando o problema errado.

maxhawkins
fonte
2

O caminho certo é aquele com o qual você se sente mais confortável e que faz o que deseja. Na programação, raramente existe uma maneira 'correta' de fazer as coisas, mais frequentemente há várias maneiras de escolher.

Se você se sentir confortável com certa maneira de fazer as coisas, faça exatamente isso, a menos que não funcione - então é hora de encontrar uma maneira melhor.

Dariusz G. Jagielski
fonte
2

No Ruby 2.1, cada método com índice é removido. Em vez disso, você pode usar each_index

Exemplo:

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

produz:

0 -- 1 -- 2 --
Amjed Shareef
fonte
4
Isso não é verdade. Conforme declarado nos comentários da resposta aceita, o motivo each_with_indexnão aparece na documentação é porque ela é fornecida pelo Enumerablemódulo.
Ihaztehcodez
0

Usar o mesmo método para iterar através de matrizes e hashes faz sentido, por exemplo, para processar estruturas de hash e matriz aninhadas, geralmente resultantes de analisadores, da leitura de arquivos JSON etc.

Uma maneira inteligente que ainda não foi mencionada é como ela é feita na biblioteca de extensões de biblioteca padrão do Ruby Facets . A partir daqui :

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

Já existe Hash#each_pair, um apelido de Hash#each. Portanto, após esse patch, também temos Array#each_paire podemos usá-lo de forma intercambiável para percorrer Hashes e Arrays. Isso corrige a insanidade observada do OP que Array#each_with_indextem os argumentos do bloco revertidos em comparação com Hash#each. Exemplo de uso:

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
fonte