【问题标题】:Ruby : Choosing between each, map, inject, each_with_index and each_with_objectRuby:在 each、map、inject、each_with_index 和 each_with_object 之间进行选择
【发布时间】:2017-03-21 01:12:15
【问题描述】:

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

在官方文档和manyStackOverflowquestions的帮助下,我慢慢开始明白那些方法的作用了。

这是我花了更长的时间才理解的:

  • 为什么要使用一种方法或另一种方法?
  • 有什么指导方针吗?

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

【问题讨论】:

  • 我觉得这个问题太笼统了(抱歉)。你可以写一本关于 Enumerable 的小书。 Ruby 的官方文档已经有好几页了。可能更适合stackoverflow.com/documentation
  • 官方文档太长,对于初学者来说可能太枯燥了。如果您正在寻找一些特定的操作,您基本上必须阅读每种方法的描述。 stackoverflow 上的相关问题是关于特定方法的,您已经需要知道方法名称才能找到它。我想写一些介于两者之间的东西。它可能很长,但它是分类的,如果标题不适合您的目标,您可以跳到下一个。

标签: arrays ruby hash enumerable


【解决方案1】:

一个更tl;dr的答案:

each、map、inject、each_with_index和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 }
    

【讨论】:

    【解决方案2】:

    我可以使用哪个对象?

    首先,您正在使用的对象应该是 ArrayHashSetRange 或任何其他响应 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 中获取一个数组

    只需致电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)仅针对数组定义。

    我想得到一个基于原始对象的修改后的数组。

    如果你想得到一个基于原始对象的Array,你可以使用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]
    

    返回的数组不会替换原来的对象。

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

    collectmap 的同义词。确保在您的项目中只使用两者之一。

    我想在原始哈希的基础上得到一个修改后的哈希。

    如果你的原始对象是一个哈希,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}
    

    我想过滤一些元素。

    我想删除 nil 元素

    您可以拨打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(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 返回原始对象。

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

    你可以使用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相比,两个块变量的顺序是相反的。

    如果您的变量是 Hash,您可能更喜欢这种注入方法,因为 h["a"]=1 返回 1,并且需要在注入块中多一行才能返回 Hash。

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

    那么使用each 可能没问题;)

    注意事项:

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

    【讨论】:

    • ('a'..'z').zip(1..26).to_h:)
    • 优秀。不过,我需要为 each_with_object 提供一个示例。
    猜你喜欢
    • 2012-11-27
    • 2011-04-23
    • 2011-01-29
    • 2010-12-06
    • 2012-04-13
    • 2013-06-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多