Qual è il modo "giusto" per iterare attraverso un array in Ruby?


341

PHP, nonostante tutte le sue verruche, è abbastanza bravo su questo punto. Non c'è differenza tra un array e un hash (forse sono ingenuo, ma questo ovviamente mi sembra giusto), e per iterare attraverso entrambi

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

In Ruby ci sono molti modi per fare questo genere di cose:

array.length.times do |i|
end

array.each

array.each_index

for i in array

Gli hash hanno più senso, dato che lo uso sempre

hash.each do |key, value|

Perché non posso farlo per gli array? Se voglio ricordare solo un metodo, immagino di poterlo usare each_index(dal momento che rende disponibili sia l'indice che il valore), ma è fastidioso doverlo fare array[index]invece che solo value.


Oh giusto, me ne sono dimenticato array.each_with_index. Tuttavia, questo fa schifo perché va |value, key|e hash.eachviene |key, value|! Non è folle?


1
Immagino array#each_with_indexusi |value, key|perché il nome del metodo implica l'ordine, mentre l'ordine utilizzato per hash#eachimita la hash[key] = valuesintassi?
Benjineer,

3
Se hai appena iniziato con i loop in Ruby, dai un'occhiata a Seleziona, Rifiuta, Raccogli, Inietta e Rileva
Matthew Carriere,

Risposte:


560

Questo ripeterà tutti gli elementi:

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

stampe:

1
2
3
4
5
6

Questo ripeterà tutti gli elementi che ti danno il valore e l'indice:

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

stampe:

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

Non sono sicuro della tua domanda quale stai cercando.


1
Fuori tema Non riesco a trovare each_with_index in ruby ​​1.9 in array
CantGetANick

19
@CantGetANick: la classe Array include il modulo Enumerable che ha questo metodo.
xuinkrbin.

88

Penso che non esista un modo giusto . Esistono molti modi diversi per iterare e ognuno ha la sua nicchia.

  • each è sufficiente per molti usi, poiché spesso non mi interessano gli indici.
  • each_ with _index si comporta come Hash # ciascuno - ottieni il valore e l'indice.
  • each_index- solo gli indici. Non lo uso spesso. Equivalente a "length.times".
  • map è un altro modo per iterare, utile quando si desidera trasformare un array in un altro.
  • select è l'iteratore da utilizzare quando si desidera scegliere un sottoinsieme.
  • inject è utile per generare somme o prodotti o per raccogliere un singolo risultato.

Può sembrare molto da ricordare, ma non preoccuparti, puoi cavartela senza conoscerli tutti. Ma quando inizierai a imparare e ad usare i diversi metodi, il tuo codice diventerà più pulito e chiaro e sarai sulla buona strada per la padronanza di Ruby.


Bella risposta! Vorrei menzionare il contrario come #reject e gli alias come #collect.
Emily Tui Ch

60

Non sto dicendo che Array-> |value,index|e Hash-> |key,value|non sia folle (vedi il commento di Horace Loeb), ma sto dicendo che c'è un modo sano di aspettarsi questo accordo.

Quando mi occupo di array, mi concentro sugli elementi dell'array (non sull'indice perché l'indice è transitorio). Il metodo è ciascuno con indice, ovvero ogni + indice o | ciascuno, indice | o |value,index|. Ciò è anche coerente con l'indice visualizzato come argomento facoltativo, ad esempio | valore | è equivalente a | valore, indice = zero | che è coerente con | valore, indice |.

Quando ho a che fare con gli hash, spesso mi concentro più sulle chiavi che sui valori, e di solito mi occupo di chiavi e valori in quell'ordine, o key => valueo hash[key] = value.

Se si desidera digitare l'anatra, utilizzare esplicitamente un metodo definito come mostrato da Brent Longborough o un metodo implicito come mostra maxhawkins.

Ruby si occupa di sistemare la lingua in base al programmatore, non del programmatore che si adatta alla lingua. Ecco perché ci sono così tanti modi. Ci sono così tanti modi di pensare a qualcosa. In Ruby, scegli il più vicino e il resto del codice di solito cade in modo estremamente accurato e conciso.

Per quanto riguarda la domanda originale, "Qual è il modo" giusto "per iterare attraverso un array in Ruby?", Beh, penso che il modo principale (cioè senza zucchero sintattico o potere orientato agli oggetti) sia quello di fare:

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

Ma Ruby è incentrato sul potente zucchero sintattico e sul potere orientato agli oggetti, ma comunque qui è l'equivalente degli hash e le chiavi possono essere ordinate o meno:

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

Quindi, la mia risposta è: "Il modo" giusto "di scorrere un array in Ruby dipende da te (cioè dal programmatore o dal team di programmazione) e dal progetto". Il programmatore Ruby migliore fa la scelta migliore (di quale potere sintattico e / o quale approccio orientato agli oggetti). Il miglior programmatore di Ruby continua a cercare altri modi.


Ora, voglio fare un'altra domanda, "Qual è il modo" giusto "per scorrere indietro un intervallo in Ruby?"! (Questa domanda è come sono arrivato a questa pagina.)

È bello da fare (per gli attaccanti):

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

ma non mi piace fare (per il passato):

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

Beh, in realtà non mi dispiace farlo troppo, ma quando insegno a tornare indietro, voglio mostrare ai miei studenti una bella simmetria (cioè con una differenza minima, ad esempio aggiungendo solo un contrario, o un passaggio -1, ma senza modificando qualcos'altro). Puoi fare (per simmetria):

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

e

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

cosa che non mi piace molto, ma non puoi farlo

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

Alla fine potresti farlo

class Range

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

end

ma voglio insegnare il puro Ruby piuttosto che gli approcci orientati agli oggetti (per ora). Vorrei ripetere all'indietro:

  • senza creare un array (considerare 0..1000000000)
  • lavorare per qualsiasi intervallo (ad es. stringhe, non solo numeri interi)
  • senza usare alcun potere orientato agli oggetti extra (cioè nessuna modifica di classe)

Credo che ciò sia impossibile senza definire un predmetodo, il che significa modificare la classe Range per usarla. Se puoi farlo per favore fatemelo sapere, altrimenti la conferma dell'impossibilità sarebbe apprezzata anche se sarebbe deludente. Forse Ruby 1.9 risolve questo problema.

(Grazie per il tuo tempo a leggere questo.)


5
1.upto(10) do |i| puts i ende in 10.downto(1) do puts i endqualche modo dà la simmetria che volevi. Spero che aiuti. Non sono sicuro che funzionerebbe per stringhe e simili.
Suren,

(1..10) .to_a.sort {| x, y | y <=> x} .each {| i | mette "i = # {i}"} - il contrario è più lento.
Davidslv,

Puoi [*1..10].each{|i| puts "i=#{i}" }.
Hauleth,

19

Usa each_with_index quando hai bisogno di entrambi.

ary.each_with_index { |val, idx| # ...

12

Le altre risposte vanno bene, ma volevo sottolineare un'altra cosa periferica: gli array sono ordinati, mentre gli hash non sono in 1.8. (In Ruby 1.9, gli hash sono ordinati in base all'ordine di inserimento delle chiavi.) Quindi non avrebbe senso prima di 1.9 iterare su un hash nello stesso modo / sequenza degli array, che hanno sempre avuto un ordinamento definito. Non so quale sia l'ordine predefinito per gli array associativi PHP (a quanto pare il mio google fu non è abbastanza forte nemmeno per capirlo), ma non so come si possano considerare gli array PHP regolari e gli array associativi PHP per essere "lo stesso" in questo contesto, poiché l'ordine per gli array associativi sembra non definito.

Come tale, il modo Ruby mi sembra più chiaro e intuitivo. :)


1
hash e array sono la stessa cosa! gli array mappano numeri interi su oggetti e gli hash mappano oggetti su oggetti. le matrici sono solo casi speciali di hash, no?
Tom Lehman,

1
Come ho detto, un array è un set ordinato. Una mappatura (in senso generico) non è ordinata. Se si limita il set di chiavi a numeri interi (come ad esempio con le matrici), accade che il set di chiavi abbia un ordine. In una mappatura generica (Hash / Associative Array), le chiavi potrebbero non avere un ordine.
Pistos,

@Horace: hash e array non sono gli stessi. Se uno è un acase speciale dell'altro, non possono essere gli stessi. Ma peggio ancora, le matrici non sono un tipo speciale di hash, è solo un modo astratto di visualizzarle. Come sottolineato da Brent, l'uso di hash e array in modo intercambiabile potrebbe indicare un odore di codice.
Zane,

9

Cercare di fare la stessa cosa in modo coerente con array e hash potrebbe essere solo un odore di codice, ma, a rischio che io sia marchiato come un codificatore patch-half-monkey, se stai cercando un comportamento coerente, questo farebbe il trucco ?:

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

7

Ecco le quattro opzioni elencate nella tua domanda, disposte in base alla libertà di controllo. Potresti volerne usare uno diverso a seconda di ciò che ti serve.

  1. Basta passare attraverso i valori:

    array.each
  2. Basta passare attraverso gli indici:

    array.each_index
  3. Passa attraverso gli indici + variabile indice:

    for i in array
  4. Conteggio loop di controllo + variabile indice:

    array.length.times do | i |

5

Avevo cercato di creare un menu (in Camping e Markaby ) usando un hash.

Ogni elemento ha 2 elementi: un'etichetta di menu e un URL , quindi un hash sembrava giusto, ma l'URL '/' per 'Home' è sempre apparso per ultimo (come ti aspetteresti per un hash), quindi le voci di menu sono apparse in modo errato ordine.

L'uso di un array con each_slicefa il lavoro:

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

L'aggiunta di valori extra per ciascuna voce di menu (ad es. Come un nome ID CSS ) significa solo aumentare il valore della sezione. Quindi, come un hash ma con gruppi costituiti da un numero qualsiasi di elementi. Perfetto.

Quindi, questo è solo per dire grazie per aver accennato inavvertitamente una soluzione!

Ovvio, ma vale la pena affermare: suggerisco di verificare se la lunghezza dell'array è divisibile per il valore dello slice.


4

Se usi il mixin enumerabile (come fa Rails) puoi fare qualcosa di simile allo snippet php elencato. Usa il metodo each_slice e appiattisci l'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}" }

Richiede meno patch di scimmia.

Tuttavia, ciò causa problemi quando si dispone di una matrice ricorsiva o di un hash con valori di matrice. In ruby ​​1.9 questo problema è risolto con un parametro al metodo appiattito che specifica quanto in profondità ricorrere.

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

Per quanto riguarda la questione se questo è un odore di codice, non sono sicuro. Di solito quando devo piegarmi all'indietro per scorrere su qualcosa, faccio un passo indietro e mi rendo conto che sto affrontando il problema in modo sbagliato.


2

Il modo giusto è quello con cui ti senti più a tuo agio e che fa quello che vuoi che faccia. Nella programmazione c'è raramente un modo 'corretto' di fare le cose, più spesso ci sono diversi modi di scegliere.

Se ti senti a tuo agio con un certo modo di fare le cose, fallo e basta, a meno che non funzioni, allora è tempo di trovare il modo migliore.


2

In Ruby 2.1, il metodo each_with_index viene rimosso. Invece puoi usare each_index

Esempio:

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

produce:

0 -- 1 -- 2 --

4
Questo non è vero. Come indicato nei commenti della risposta accettata, il motivo each_with_indexnon appare nella documentazione perché è fornito dal Enumerablemodulo.
ihaztehcodez,

0

Usare lo stesso metodo per scorrere tra array e hash ha senso, ad esempio per elaborare strutture hash-e-array nidificate spesso risultanti da parser, dalla lettura di file JSON ecc.

Un modo intelligente che non è stato ancora menzionato è il modo in cui viene fatto nella libreria Ruby Facets delle estensioni standard della libreria. Da qui :

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

C'è già Hash#each_pairun alias di Hash#each. Quindi, dopo questa patch, abbiamo anche Array#each_paire possiamo usarla in modo intercambiabile per iterare sia con Hash che con Array. Questo risolve la follia osservata dell'OP che Array#each_with_indexha gli argomenti a blocchi invertiti rispetto a Hash#each. Esempio di utilizzo:

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, !"
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.