ChatGPT解决这个技术问题 Extra ChatGPT

What is the "right" way to iterate through an array in Ruby?

PHP, for all its warts, is pretty good on this count. There's no difference between an array and a hash (maybe I'm naive, but this seems obviously right to me), and to iterate through either you just do

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

In Ruby there are a bunch of ways to do this sort of thing:

array.length.times do |i|
end

array.each

array.each_index

for i in array

Hashes make more sense, since I just always use

hash.each do |key, value|

Why can't I do this for arrays? If I want to remember just one method, I guess I can use each_index (since it makes both the index and value available), but it's annoying to have to do array[index] instead of just value.

Oh right, I forgot about array.each_with_index. However, this one sucks because it goes |value, key| and hash.each goes |key, value|! Is this not insane?

I guess array#each_with_index uses |value, key| because the method name implies the order, whereas the order used for hash#each mimics the hash[key] = value syntax?
If you are just getting started with loops in Ruby, then check out using select, reject, collect, inject and detect

A
Aryan Beezadhur

This will iterate through all the elements:

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

# Output:

1
2
3
4
5
6

This will iterate through all the elements giving you the value and the index:

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

# Output:

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

I'm not quite sure from your question which one you are looking for.


As of Ruby 2.7, also array.each { puts _1 }
A
AShelly

I think there is no one right way. There are a lot of different ways to iterate, and each has its own niche.

each is sufficient for many usages, since I don't often care about the indexes.

each_ with _index acts like Hash#each - you get the value and the index.

each_index - just the indexes. I don't use this one often. Equivalent to "length.times".

map is another way to iterate, useful when you want to transform one array into another.

select is the iterator to use when you want to choose a subset.

inject is useful for generating sums or products, or collecting a single result.

It may seem like a lot to remember, but don't worry, you can get by without knowing all of them. But as you start to learn and use the different methods, your code will become cleaner and clearer, and you'll be on your way to Ruby mastery.


great answer! I'd like to mention the reverse like #reject and aliases like #collect.
佚名

I'm not saying that Array -> |value,index| and Hash -> |key,value| is not insane (see Horace Loeb's comment), but I am saying that there is a sane way to expect this arrangement.

When I am dealing with arrays, I am focused on the elements in the array (not the index because the index is transitory). The method is each with index, i.e. each+index, or |each,index|, or |value,index|. This is also consistent with the index being viewed as an optional argument, e.g. |value| is equivalent to |value,index=nil| which is consistent with |value,index|.

When I am dealing with hashes, I am often more focused on the keys than the values, and I am usually dealing with keys and values in that order, either key => value or hash[key] = value.

If you want duck-typing, then either explicitly use a defined method as Brent Longborough showed, or an implicit method as maxhawkins showed.

Ruby is all about accommodating the language to suit the programmer, not about the programmer accommodating to suit the language. This is why there are so many ways. There are so many ways to think about something. In Ruby, you choose the closest and the rest of the code usually falls out extremely neatly and concisely.

As for the original question, "What is the “right” way to iterate through an array in Ruby?", well, I think the core way (i.e. without powerful syntactic sugar or object oriented power) is to do:

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

But Ruby is all about powerful syntactic sugar and object oriented power, but anyway here is the equivalent for hashes, and the keys can be ordered or not:

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

So, my answer is, "The “right” way to iterate through an array in Ruby depends on you (i.e. the programmer or the programming team) and the project.". The better Ruby programmer makes the better choice (of which syntactic power and/or which object oriented approach). The better Ruby programmer continues to look for more ways.

Now, I want to ask another question, "What is the “right” way to iterate through a Range in Ruby backwards?"! (This question is how I came to this page.)

It is nice to do (for the forwards):

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

but I don't like to do (for the backwards):

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

Well, I don't actually mind doing that too much, but when I am teaching going backwards, I want to show my students a nice symmetry (i.e. with minimal difference, e.g. only adding a reverse, or a step -1, but without modifying anything else). You can do (for symmetry):

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

and

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

which I don't like much, but you can't do

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

You could ultimately do

class Range

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

end

but I want to teach pure Ruby rather than object oriented approaches (just yet). I would like to iterate backwards:

without creating an array (consider 0..1000000000)

working for any Range (e.g. Strings, not just Integers)

without using any extra object oriented power (i.e. no class modification)

I believe this is impossible without defining a pred method, which means modifying the Range class to use it. If you can do this please let me know, otherwise confirmation of impossibility would be appreciated though it would be disappointing. Perhaps Ruby 1.9 addresses this.

(Thanks for your time in reading this.)


1.upto(10) do |i| puts i end and 10.downto(1) do puts i end kind of gives the symmetry you wanted. Hope that helps. Not sure if it would work for strings and such.
(1..10).to_a.sort{ |x,y| y <=> x }.each{|i| puts "i=#{i}" } - reverse is more slow.
You can [*1..10].each{|i| puts "i=#{i}" }.
J
J Cooper

Use each_with_index when you need both.

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

P
Pistos

The other answers are just fine, but I wanted to point out one other peripheral thing: Arrays are ordered, whereas Hashes are not in 1.8. (In Ruby 1.9, Hashes are ordered by insertion order of keys.) So it wouldn't make sense prior to 1.9 to iterate over a Hash in the same way/sequence as Arrays, which have always had a definite ordering. I don't know what the default order is for PHP associative arrays (apparently my google fu isn't strong enough to figure that out, either), but I don't know how you can consider regular PHP arrays and PHP associative arrays to be "the same" in this context, since the order for associative arrays seems undefined.

As such, the Ruby way seems more clear and intuitive to me. :)


hashes and arrays are the same thing! arrays map integers to objects and hashes map objects to objects. arrays are just special cases of hashes, no?
Like I said, an array is an ordered set. A mapping (in the generic sense) is unordered. If you restrict the key set to integers (such as with arrays), it just so happens that the key set has an order. In a generic mapping (Hash / Associative Array), the keys may not have an order.
@Horace: Hashes and arrays are not the same. If one is a special acase of the other, they cannot be the same. But even worse, arrays are not a special kind of hashes, that's only an abstract way of viewing them. As Brent above pointed out, using hashes and arrays interchangeably might indicate a code smell.
e
emlai

Here are the four options listed in your question, arranged by freedom of control. You might want to use a different one depending on what you need.

Simply go through values: array.each Simply go through indices: array.each_index Go through indices + index variable: for i in array Control loop count + index variable: array.length.times do | i |


B
Brent.Longborough

Trying to do the same thing consistently with arrays and hashes might just be a code smell, but, at the risk of my being branded as a codorous half-monkey-patcher, if you're looking for consistent behaviour, would this do the trick?:

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

D
Dave Everitt

I'd been trying to build a menu (in Camping and Markaby) using a hash.

Each item has 2 elements: a menu label and a URL, so a hash seemed right, but the '/' URL for 'Home' always appeared last (as you'd expect for a hash), so menu items appeared in the wrong order.

Using an array with each_slice does the job:

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

Adding extra values for each menu item (e.g. like a CSS ID name) just means increasing the slice value. So, like a hash but with groups consisting of any number of items. Perfect.

So this is just to say thanks for inadvertently hinting at a solution!

Obvious, but worth stating: I suggest checking if the length of the array is divisible by the slice value.


m
maxhawkins

If you use the enumerable mixin (as Rails does) you can do something similar to the php snippet listed. Just use the each_slice method and flatten the 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}" }

Less monkey-patching required.

However, this does cause problems when you have a recursive array or a hash with array values. In ruby 1.9 this problem is solved with a parameter to the flatten method that specifies how deep to recurse.

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

As for the question of whether this is a code smell, I'm not sure. Usually when I have to bend over backwards to iterate over something I step back and realize I'm attacking the problem wrong.


k
kaybee99

In Ruby 2.1, each_with_index method is removed. Instead you can use each_index

Example:

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

produces:

0 -- 1 -- 2 --

This is not true. As stated in the comments of the accepted answer, the reason each_with_index does not appear in the documentation is because it is provided by the Enumerable module.
D
Dariusz G. Jagielski

The right way is the one you feel most comfortable with and which does what you want it to do. In programming there is rarely one 'correct' way to do things, more often there are multiple ways to choose.

If you are comfortable with certain way of doings things, do just it, unless it doesn't work - then it is time to find better way.


t
tanius

Using the same method for iterating through both arrays and hashes makes sense, for example to process nested hash-and-array structures often resulting from parsers, from reading JSON files etc..

One clever way that has not yet been mentioned is how it's done in the Ruby Facets library of standard library extensions. From here:

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

There is already Hash#each_pair, an alias of Hash#each. So after this patch, we also have Array#each_pair and can use it interchangeably to iterate through both Hashes and Arrays. This fixes the OP's observed insanity that Array#each_with_index has the block arguments reversed compared to Hash#each. Example usage:

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, !"