Ruby:在每个,map,inject,each_with_index和each_with_object之间进行选择

时间:2016-11-07 16:04:41

标签: arrays ruby hash enumerable

当我多年前开始编写Ruby时,我花了一些时间来理解eachmap之间的区别。当我发现所有其他EnumerableArray方法时,情况会变得更糟。

在官方文档和many StackOverflow questions的帮助下,我慢慢开始了解这些方法的作用。

然而,这让我更难理解:

  • 我为什么要使用一种方法?
  • 有没有指导方针?

我希望这个问题不重复:我对“为什么?”更感兴趣。比“什么?”或“如何?”,我认为它可以帮助Ruby新人。

2 个答案:

答案 0 :(得分:17)

更多 tl; dr 回答:

  

如何在每个,地图,注入,each_with_index和each_with_object之间进行选择?

  • 当您需要" generic" 迭代并且不关心结果时,请使用#each。示例 - 您有数字,您想要打印每个单独数字的绝对值:

    numbers.each { |number| puts number.abs }
    
  • 如果需要新列表,请使用#map,其中每个元素通过转换原始元素以某种方式形成。示例 - 您有数字,想要得到它们的正方形:

    numbers.map { |number| number ** 2 }
    
  • 如果您想以某种方式将整个列表减少为一个值,请使用#inject。示例 - 您有数字,您想得到它们的总和:

    numbers.inject(&:+)
    
  • 在与#each_with_index相同的情况下使用#each,除了您还希望索引包含每个元素:

    numbers.each_with_index { |number, index| puts "Number #{number} is on #{index} position" }
    
  • #each_with_object的用途更为有限。最常见的情况是,如果您需要类似#inject的内容,但需要一个新的集合(而不是奇异值),这不是原始的直接映射。示例 - 数字直方图(频率):

    numbers.each_with_object({}) { |number, histogram| histogram[number] = histogram[number].to_i.next }
    

答案 1 :(得分:4)

我可以使用哪个对象?

首先,您正在使用的对象应该是ArrayHashSetRange或任何其他响应{{}的对象3}}。如果没有,它可能会转换成会发生的事情。例如,您无法直接在each上调用each,因为您需要指定是否要迭代每个字节,字符或行。

"Hello World".respond_to?(:each)
#=> false
"Hello World".each_char.respond_to?(:each) 
#=> true

我想用每个元素计算一些东西,就像用C或Java中的for循环一样。

如果要迭代每个元素,对其执行某些操作而不修改原始对象,则可以使用String。请继续阅读,以了解你是否真的应该。

array = [1,2,3]

#NOTE: i is a bound variable, it could be replaced by anything else (x, n, element). It's a good idea to use a descriptive name if you can
array.each do |i|
  puts "La"*i
end
#=> La
#   LaLa
#   LaLaLa

这是最通用的迭代方法,您可以使用它编写任何其他提到的方法。实际上,我们只会出于教学目的。如果您在代码中发现了类似的模式,则可以使用相应的方法替换它。

使用each基本上没有错,但它几乎不是最好的选择。它很冗长而不是Ruby-ish。

请注意each返回原始对象,但这很少(从不?)使用。逻辑发生在块内,不应修改原始对象。

我使用each的唯一时间是:

  • 没有其他方法可以做。我对Ruby的了解越多,发生的次数就越少。
  • 当我为不懂Ruby的人编写脚本时,有一些编程经验(例如C,Fortran,VBA),并希望了解我的代码。

我想从我的String / Hash / Set / File / Range / ActiveRecord :: Relation

中获取一个数组

只需致电object.to_a

(1..10).to_a
#=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
"Hello world".each_char.to_a
#=> ["H", "e", "l", "l", "o", " ", "w", "o", "r", "l", "d"]
{:a => 1, :b => 2}.to_a
#=> [[:a, 1], [:b, 2]]
Movie.all.to_a #NOTE: Probably very inefficient. Try to keep an ActiveRecord::Relation as Relation for as long as possible.
#=> [Citizen Kane, Trois couleurs: Rouge, The Grapes of Wrath, ....

下面介绍的一些方法(例如eachcompact)仅为数组定义。

我想根据原始对象获取修改后的数组。

如果要根据原始对象获取阵列,可以使用uniq。返回的对象将与原始对象具有相同的大小。

array = [1,2,3]

new_array = array.map do |i|
  i**2
end
new_array
#=> [1, 4, 9]

#NOTE: map is often used in conjunction with other methods. Here is the corresponding one-liner, without creating a new variable :
array.map{|i| i**2}
#=> [1, 4, 9]

# EACH-equivalent (For pedagogical purposes only):
new_array = []
array.each do |i|
  new_array << i**2
end
new_array
#=> [1, 4, 9]

返回的Array不会替换原始对象。

这种方法应用非常广泛。它应该是您在map之后学习的第一个。

eachcollect的同义词。确保在项目中只使用其中一个。

我希望根据原始Hash获得修改后的Hash。

如果您的原始对象是哈希,map将返回一个数组。如果你想要哈希:

hash = {a: 1, b: 2}
hash.map{|key, value| [key, value*2]}.to_h
#=> {:a=>2, :b=>4}

# EACH-equivalent
hash = {a: 1, b: 2}
new_hash = {}
hash.each do |key,value|
  new_hash[key]=value*2
end
new_hash
#=> {:a=>2, :b=>4}

我想过滤一些元素。

我想删除无元素

您可以致电map。它将返回一个没有nil元素的新数组。

array = [1,2,nil,4,5]

#NOTE: array.map{|i| i*2} Would raise a NoMethodError
array.compact
# => [1, 2, 4, 5]

# EACH-equivalent
new_array = []
array.each do |integer_or_nil|
  new_array << integer_or_nil unless integer_or_nil.nil?
end
new_array

我想写一些逻辑来确定元素是否应该保存在新数组

您可以使用compactselect

integers = (1..10)
integers.select{|i| i.even?}
# => [2, 4, 6, 8, 10]
integers.reject{|i| i.odd?}
# => [2, 4, 6, 8, 10]

# EACH-equivalent
new_array = []
integers.each do |i|
    new_array << i if i.even?
end
new_array

我想从数组中删除重复的元素

您可以使用reject

letters = %w(a b a b c)
letters.uniq
#=> ["a", "b", "c"]

# EACH-equivalent
uniq_letters = []
letters.each do |letter|
  uniq_letters << letter unless uniq_letters.include?(letter)
end
uniq_letters

#TODO: Add find/detect/any?/all?/count
#TODO: Add group_by/sort/sort_by

我想迭代所有元素,同时从0到n-1

进行计数

您可以使用uniq

letters = %w(a b c)
letters.each_with_index do |letter, i|
  puts "Letter ##{i} : #{letter}"
end
#=> Letter #0 : a
#   Letter #1 : b
#   Letter #2 : c

#NOTE: There's a nice Ruby syntax if you want to use each_with_index with a Hash
hash = {:a=>1, :b=>2}
hash.each_with_index{|(key,value),i| puts "#{i} : #{key}->#{value}"}
# => 0 : a->1
#    1 : b->2

# EACH-equivalent
i = 0
letters.each do |letter|
  puts "Letter ##{i} : #{letter}"
  i+=1
end

each_with_index返回原始对象。

我想迭代所有元素,同时在每次迭代期间设置变量并在下一次迭代中使用它。

您可以使用each_with_index

gauss = (1..100)
gauss.inject{|sum, i| sum+i}
#=> 5050
#NOTE: You can specify a starting value with gauss.inject(0){|sum, i| sum+i}

# EACH-equivalent
sum = 0
gauss.each do |i|
  sum = sum + i
end
puts sum

它返回上次迭代定义的变量。

inject是同义词。与map / collect一样,选择一个关键字并保留它。

我想迭代所有元素,同时保持变量可用于每次迭代。

您可以使用reduce

letter_ids = (1..26)

letter_ids.each_with_object({}){|i,alphabet| alphabet[("a".ord+i-1).chr]=i}
#=> {"a"=>1, "b"=>2, "c"=>3, "d"=>4, "e"=>5, "f"=>6, "g"=>7, "h"=>8, "i"=>9, "j"=>10, "k"=>11, "l"=>12, "m"=>13, "n"=>14, "o"=>15, "p"=>16, "q"=>17, "r"=>18, "s"=>19, "t"=>20, "u"=>21, "v"=>22, "w"=>23, "x"=>24, "y"=>25, "z"=>26}

# EACH-equivalent
alphabet = {}
letter_ids.each do |i|
  letter = ("a".ord+i-1).chr
  alphabet[letter]=i
end
alphabet

返回上次迭代修改的变量。请注意,与each_with_object相比,两个块变量的顺序相反。

如果你的变量是Hash,你应该更喜欢这种方法注入,因为h["a"]=1返回1,并且你的注入块需要多一行来返回哈希。

我想要一些尚未提及的内容。

然后可以使用inject;)

注意:

这是一项正在进行中的工作,我很乐意感谢任何反馈。如果它足够有趣并且适合一页,我可以从中提取一个流程图。