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

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

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

以下是让我更长时间理解的内容:

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

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

更多的答案: 博士回答:

如何在每个,map,inject,each_with_index和each_with_object之间进行选择?

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

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

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

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

     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 } 

我可以使用哪个对象?

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

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

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

如果你想迭代每个元素,用它做一些事情而不是修改原始对象,你可以使用each元素。 请继续阅读,以了解你是否真的应该。

 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中获取一个Array

只需调用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, .... 

下面描述的一些方法(例如compactuniq )仅为数组定义。

我想基于原始对象获得修改过的数组。

如果要基于原始对象获取arrays,可以使用map 。 返回的对象将具有与原始对象相同的大小。

 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不会替换原始对象。

这种方法应用非常广泛。 它应该是你在each之后学到的第一个。

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

我希望得到一个基于原始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} 

我想过滤一些元素。

我想删除零元素

你可以打电话给compact 。 它将返回一个没有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 

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

您可以使用selectreject

 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 

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

你可以使用uniq

 letters = %w(ababc) 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计数

你可以使用each_with_index

 letters = %w(abc) 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返回原始对象。

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

你可以使用inject

 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 

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

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

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

你可以使用each_with_object

 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 

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

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

我想要一些尚未提及的东西。

然后可以使用each ;)

备注:

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