在Ruby中,迭代数组的“正确”方式是什么?

403

就其所有缺陷而言,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中的循环,那么请查看使用select、reject、collect、inject和detect - Matthew Carriere
12个回答

639

这将遍历所有元素:

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

# Output:

1
2
3
4
5
6

这将遍历所有元素,提供值和索引:
array = ["A", "B", "C"]
array.each_with_index {|val, index| puts "#{val} => #{index}" }

# Output:

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

从你的问题中,我不太确定你具体需要哪一个。


1
从 Ruby 2.7 开始,也可以使用 array.each { puts _1 } - Chris

118

我认为没有一种正确的方法。有很多不同的迭代方式,每一种都有其自己的特点。

  • each 很适合许多用途,因为通常并不关心索引。
  • each_with_index 类似于 Hash#each - 您可以获取值和索引。
  • each_index - 仅限索引。 我不经常使用此方法。 相当于 "length.times"。
  • map 是另一种迭代方式,当您想将一个数组转换为另一个数组时很有用。
  • select 是选择子集时要使用的迭代器。
  • inject 对于生成总和或乘积,或收集单个结果非常有用。

可能看起来需要记住很多,但别担心,您可以在不知道它们所有方法的情况下进行编码。但是,随着您开始学习和使用不同的方法,您的代码将变得更加清晰简洁,您将朝着Ruby掌握的方向前进。


好棒的答案!我想提一下像 #reject 这样的反向方法和 #collect 的别名。 - Sandra Cieseck

64
我并不是说 Array -> |value,index|Hash -> |key,value| 这样的写法不合理(可以看 Horace Loeb 的评论),但我认为有一种更加合理的写法。
当我处理数组时,我关注的是数组中的元素(而非索引,因为索引是暂时的)。这个方法是 each with index,即 each+index,或 |each,index|,或 |value,index|。这也与将索引视为可选参数相符,例如 |value| 相当于 |value,index=nil|,这与 |value,index| 是一致的。
当我处理哈希表时,我通常更关注键而不是值,并且通常按照键和值的顺序处理,要么是 key => value,要么是 hash[key] = value
如果你想使用鸭子类型,那么可以像 Brent Longborough 所示明确地使用已定义的方法,也可以像 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}" }

但我不喜欢做(向后):


(注意:保留了HTML标签)
(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 end10.downto(1) do puts i end 这样的代码可以呈现出你想要的对称性。希望这有所帮助。不确定是否适用于字符串等其他情况。 - Suren
(1..10).to_a.sort{ |x,y| y <=> x }.each{|i| puts "i=#{i}" } - 反转更慢。 - Davidslv
你可以使用[*1..10].each{|i| puts "i=#{i}" } - Hauleth

20

当需要两者时,请使用each_with_index。

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

12
其他答案都不错,但我想指出另外一个次要的问题:在1.8版本中,数组是有序的,而哈希表则不是。(在Ruby 1.9中,哈希表按照键的插入顺序排序。)因此,在1.9之前,以与数组相同的方式/顺序迭代哈希表是没有意义的,因为数组总是具有明确定义的顺序。我不知道PHP关联数组的默认顺序是什么(显然我的谷歌功夫不够强大,无法弄清楚),但在这种情况下,我不知道如何将普通PHP数组和PHP关联数组视为“相同”,因为关联数组的顺序似乎是未定义的。
因此,对我来说,Ruby的方法更清晰、更直观。 :)

2
哈希和数组是一样的东西!数组将整数映射到对象,而哈希将对象映射到对象。数组只是哈希的特殊情况,不是吗? - Tom Lehman
1
就像我说的,数组是一个有序集合。映射(在通用意义上)是无序的。如果你将键集限制为整数(例如使用数组),那么键集恰好具有顺序。在通用映射(哈希/关联数组)中,键可能没有顺序。 - Pistos
@Horace:哈希和数组并不相同。如果一个是另一个的特殊情况,它们就不能相同。但更糟糕的是,数组并不是哈希的一种特殊类型,这只是一种抽象的视角。正如上面的Brent指出的那样,混用哈希和数组可能会暗示代码存在问题。 - Zane

11

以下是您问题中列出的四个选项,按控制自由度排列。根据您的需求,您可能希望使用不同的选项。

  1. 仅遍历值:

array.each
  • 只需简单地遍历索引:

    array.each_index
    
  • 通过索引和索引变量进行遍历:

  • for i in array
    
  • 控制循环次数 + 索引变量:

  • array.length.times do | i |
    

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

    5
    我一直在尝试使用哈希表(在Camping和Markaby中)构建菜单。每个项目都有两个元素:一个菜单标签和一个URL,因此使用哈希表似乎是正确的选择。但是,“主页”的“/” URL 总是出现在最后(就像哈希表的特性一样),因此菜单项的顺序是错误的。
    使用带有“each_slice”的数组可以解决这个问题:
    ['Home', '/', 'Page two', 'two', 'Test', 'test'].each_slice(2) do|label,link|
       li {a label, :href => link}
    end
    

    为每个菜单项添加额外值(例如,像CSS的ID名称)只意味着增加片段值。因此,就像哈希一样,但是组包含任意数量的项目。完美。

    所以这只是想说感谢您无意中暗示了解决方案!

    显而易见,但值得说明的是:建议检查数组长度是否可被片段值整除。


    4
    如果您使用可枚举 mixin(如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]]
    

    关于这是否是代码异味的问题,我不确定。通常当我必须费尽心思地迭代某些东西时,我会退后一步意识到我正在错误地解决问题。

    3
    在 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

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接