Ruby :在每个、映射、注入、each_with_index 和 each_with_object 之间进行选择
Ruby : Choosing between each, map, inject, each_with_index and each_with_object
当我多年前开始写 Ruby 时,我花了一段时间才理解 each and map. It only got worse when I discovered all the other Enumerable and Array 方法之间的区别。
在官方文档和many Whosebug questions的帮助下,我慢慢开始明白那些方法做了什么
这是我花了更长时间才理解的内容:
- 为什么我应该使用一种或另一种方法?
- 有什么指导方针吗?
我希望这个问题不是重复的:我对 "Why?" 比 "What?" 或 "How?" 更感兴趣,我认为它可以帮助 Ruby新人
我可以使用哪个对象?
首先,您正在使用的对象应该是 Array, a Hash, a Set, a Range or any other object that respond to each
. If it doesn't, it might be converted to something that will. You cannot call each
directly on a String,因为您需要指定是否要遍历每个字节、字符或行。
"Hello World".respond_to?(:each)
#=> false
"Hello World".each_char.respond_to?(:each)
#=> true
我想用每个元素计算一些东西,就像 C 中的 for 循环或 Java。
如果你想遍历每个元素,用它做一些事情而不修改原来的对象,你可以使用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
return 是原始对象,但这很少(从不?)使用。逻辑发生在块内部,不应修改原始对象。
我唯一一次使用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, ....
下面描述的一些方法(例如 compact
, uniq
)仅为数组定义。
我想得到一个基于原始对象的修改数组。
如果想得到一个基于原始对象的Array,可以使用map
。 returned 对象将与原始对象具有相同的大小。
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]
returned 数组不会替换原始对象。
这种方法应用非常广泛。应该是each
.
后学习的第一个
collect
是 map
的同义词。确保在您的项目中只使用两者之一。
我想在原始Hash的基础上得到修改后的Hash。
如果您的原始对象是一个散列,map
无论如何都会 return 一个数组。如果你想要哈希返回:
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}
我想过滤一些元素。
我想删除 nil 元素
您可以拨打compact
。它将 return 一个没有 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
我想编写一些逻辑来确定一个元素是否应该保留在新数组中
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(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 的同时遍历所有元素
您可以使用 each_with_index
:
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
return原始对象。
我想遍历所有元素,同时在每次迭代期间设置一个变量并在下一次迭代中使用它。
您可以使用 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
它return是最后一次迭代定义的变量。
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
它return是上次迭代修改的变量。请注意,与 inject
.
相比,两个块变量的顺序是相反的
如果你的变量是哈希,你应该更喜欢这种方法来注入,因为 h["a"]=1
returns 1,它需要在你的注入块中多一行到 return一个哈希。
我想要一些还没有提到的东西。
那用each
大概就可以了 ;)
备注:
这是一项正在进行的工作,我很乐意收到任何反馈。如果它足够有趣并且适合一页,我可能会从中提取流程图。
更多 tl;dr 答案:
How to choose between each, map, inject, each_with_index and each_with_object?
当你想要"generic"迭代而不关心结果时使用#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 }
当我多年前开始写 Ruby 时,我花了一段时间才理解 each and map. It only got worse when I discovered all the other Enumerable and Array 方法之间的区别。
在官方文档和many Whosebug questions的帮助下,我慢慢开始明白那些方法做了什么
这是我花了更长时间才理解的内容:
- 为什么我应该使用一种或另一种方法?
- 有什么指导方针吗?
我希望这个问题不是重复的:我对 "Why?" 比 "What?" 或 "How?" 更感兴趣,我认为它可以帮助 Ruby新人
我可以使用哪个对象?
首先,您正在使用的对象应该是 Array, a Hash, a Set, a Range or any other object that respond to each
. If it doesn't, it might be converted to something that will. You cannot call each
directly on a String,因为您需要指定是否要遍历每个字节、字符或行。
"Hello World".respond_to?(:each)
#=> false
"Hello World".each_char.respond_to?(:each)
#=> true
我想用每个元素计算一些东西,就像 C 中的 for 循环或 Java。
如果你想遍历每个元素,用它做一些事情而不修改原来的对象,你可以使用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
return 是原始对象,但这很少(从不?)使用。逻辑发生在块内部,不应修改原始对象。
我唯一一次使用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, ....
下面描述的一些方法(例如 compact
, uniq
)仅为数组定义。
我想得到一个基于原始对象的修改数组。
如果想得到一个基于原始对象的Array,可以使用map
。 returned 对象将与原始对象具有相同的大小。
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]
returned 数组不会替换原始对象。
这种方法应用非常广泛。应该是each
.
collect
是 map
的同义词。确保在您的项目中只使用两者之一。
我想在原始Hash的基础上得到修改后的Hash。
如果您的原始对象是一个散列,map
无论如何都会 return 一个数组。如果你想要哈希返回:
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}
我想过滤一些元素。
我想删除 nil 元素
您可以拨打compact
。它将 return 一个没有 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
我想编写一些逻辑来确定一个元素是否应该保留在新数组中
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(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 的同时遍历所有元素
您可以使用 each_with_index
:
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
return原始对象。
我想遍历所有元素,同时在每次迭代期间设置一个变量并在下一次迭代中使用它。
您可以使用 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
它return是最后一次迭代定义的变量。
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
它return是上次迭代修改的变量。请注意,与 inject
.
如果你的变量是哈希,你应该更喜欢这种方法来注入,因为 h["a"]=1
returns 1,它需要在你的注入块中多一行到 return一个哈希。
我想要一些还没有提到的东西。
那用each
大概就可以了 ;)
备注:
这是一项正在进行的工作,我很乐意收到任何反馈。如果它足够有趣并且适合一页,我可能会从中提取流程图。
更多 tl;dr 答案:
How to choose between each, map, inject, each_with_index and each_with_object?
当你想要"generic"迭代而不关心结果时使用
#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 }