【问题标题】:Why is Ruby's Array#shift used in this task?为什么在这个任务中使用 Ruby 的 Array#shift?
【发布时间】:2015-07-27 18:00:18
【问题描述】:

我正在阅读这个关于 Rubymonk 的烦人教程,它要求我执行以下操作:

为我写出三种方法——计算、加法和减法。测试应该全部通过。有问题就看提示!还有一点额外的提示:请记住,您可以使用 something.is_a?(Hash) 或 another_thing.is_a?(String) 来检查对象的类型。

我什至无法理解他们要我做什么,所以我决定看看解决方案,并努力对任务有一个体面的理解。

解决办法如下:

def add(*numbers)
  numbers.inject(0) { |sum, number| sum + number }  
end

def subtract(*numbers)
  current_result = numbers.shift
  numbers.inject(current_result) { |current_result, number| current_result - number }  
end

def calculate(*arguments)
  # if the last argument is a Hash, extract it 
  # otherwise create an empty Hash
  options = arguments[-1].is_a?(Hash) ? arguments.pop : {}
  options[:add] = true if options.empty?
  return add(*arguments) if options[:add]
  return subtract(*arguments) if options[:subtract]
end

很多东西我都不懂,但让我困惑的一件事是 shift 方法:current_result = numbers.shift。为什么会在那里?我的意思是,我理解它的作用,但它在这段特定的代码中的作用是什么?

顺便说一句,如果有人不厌其烦地为我破解这段代码,我将永远感激不尽。

任务位于以下页面的底部: https://rubymonk.com/learning/books/1-ruby-primer/chapters/19-ruby-methods/lessons/69-new-lesson#solution3899

【问题讨论】:

    标签: ruby


    【解决方案1】:

    添加(*数字)

    让我们从调用开始:

    def add(*numbers)
      numbers.inject(0) { |sum, number| sum + number }  
    end
    

    像这样:

    add(1,2,3)    #=> 6
    

    或者像这样:

    add(*[1,2,3]) #=> 6
    

    两者是等价的。后者向您展示了运算符“splat”的作用。

    这会导致:

    numbers      #=> [1,2,3]
    

    所以 Ruby 将Enumerable#inject(又名reduce)发送到numbers

    [1,2,3].inject(0) { |sum, number| sum + number }
    

    inject 首先将“备忘录”sum 初始化为inject 的参数(如果像这里一样,它有一个),然后将“接收者”[1,2,3] 的第一个元素传递到块中,然后将其分配给块变量number:

    sum          #=> 0
    number       #=> 1
    

    Ruby 然后计算:

    sum + number #=> 0 + 1 => 1
    

    成为备忘录的新值。接下来,inject2 传递到块中并计算:

    sum          #=> 1
    number       #=> 2
    sum + number #=> 3
    

    所以(备忘录)sum 现在是 3

    最后,

    sum          #=> 3
    number       #=> 3
    sum + number #=> 6
    

    由于接收者的所有元素都已传递到块中,inject 返回备忘录的值:

    sum          #=> 6
    

    如果您检查 inject 的文档,您会发现如果该方法没有参数,Ruby 会将接收者的第一个元素(此处为 1)分配给备忘录(sum),然后继续以上从接收器的第二个元素(2)开始。正如预期的那样,这会产生相同的答案:

    def add(*numbers)
      numbers.inject { |sum, number| sum + number }  
    end
    
    add(1,2,3) #=> 6
    

    那么为什么要包含参数零呢?通常我们会希望add()(即add(*[])) 返回零。我将留给您调查inject 的两种形式中的每一种会发生什么情况。您能得出什么结论?

    正如@Stefan 在他的回答中指出的那样,您可以简单地这样做:

    def add(*numbers)
      numbers.inject :+
    end
    

    这是你通常看到的写法。

    但是,如果 numbers 可能是一个空数组,您需要为备忘录提供一个零初始值:

    def add(*numbers)
      numbers.inject 0, :+
    end
    
    add(*[]) #=> 0
    

    减(*数字)

    def subtract(*numbers)
      current_result = numbers.shift
      numbers.inject(current_result) { |current_result, number|
        current_result - number }  
    end
    

    这与add 的方法类似,只是稍有不同。我们需要备忘录的第一个值(这里是current_result)作为接收者的第一个元素。我们有两种方法可以做到这一点。

    第一种方式是这样的:

    def subtract(*numbers)
      numbers[1..-1].inject(numbers.first) { |current_result, number|
        current_result - number }  
    end
    
    numbers = [6,2,3]    
    subtract(*numbers) #=> 1 
    

    first_number = numbers.first   #=> 6
    all_but_first = numbers[1..-1] #=> [2,3]
    

    然后:

    numbers[1..-1].inject(numbers.first) { ... }
    

    是:

    all_but_first.inject(first_number) { ... }
      #=> [2,3].inject(6) { ... }
    

    相反,作者选择写:

    first_number = numbers.shift   #=> 6
    numbers                        #=> [2,3]
    
    numbers.inject(first_number) { ... }
      #=> [2,3].inject(6) { ... }
    

    这可能有点漂亮,但选择权在你。

    第二种方式是不带参数使用inject

    def subtract(*numbers)
      numbers.inject { |current_result, number| current_result - number }  
    end
    
    numbers = [6,2,3]    
    subtract(*numbers) #=> 1 
    

    您可以通过查看inject 的文档来了解其工作原理。

    另外,类似:add,可以写成:

    def subtract(*numbers)
      numbers.inject :-  
    end
    

    最后,subtract 要求numbers 至少有一个元素,所以我们可以这样写:

    def subtract(*numbers)
      raise ArgumentError, "argument cannot be an empty array" if numbers.empty?
      numbers.inject :-  
    end
    

    计算(*参数)

    我们看到calculate 期望以下列方式之一被调用:

    calculate(6,2,3,{ :add=>true })      #=> 11 
    calculate(6,2,3,{ :add=>7 })         #=> 11  
    calculate(6,2,3,{ :subtract=>true }) #=> 1 
    calculate(6,2,3,{ :subtract=>7 })    #=> 1 
    calculate(6,2,3)                     #=> 11 
    

    如果散列的键 :add 具有“真实”值(除 falsenil 之外的任何值,我们将添加;如果散列具有键 :subtract 具有“真实”值(除falsenil 之外的任何内容,我们都要减去。如果最后一个元素不是散列(calculate(6,2,3)),则假定为add

    注意:

    calculate(6,2,3,{ :add=>false })     #=> nil
    calculate(6,2,3,{ :subtract=>nil })  #=> nil
    

    让我们这样写方法:

    def calculate(*arguments)
      options =
      if arguments.last.is_a?(Hash) # or if arguments.last.class==Hash 
        arguments.pop
      else
        {}
      end 
    
      if (options.empty? || options[:add])
        add *arguments
      elsif options[:subtract]
        subtract *arguments
      else
        nil
      end
    end
    
    calculate(6,2,3,{ :add=>true })      #=> 11 
    calculate(6,2,3,{ :add=>7 })         #=> 11  
    calculate(6,2,3,{ :subtract=>true }) #=> 1 
    calculate(6,2,3,{ :subtract=>7 })    #=> 1 
    calculate(6,2,3)                     #=> 11 
    calculate(6,2,3,{ :add=>false })     #=> nil
    calculate(6,2,3,{ :subtract=>nil })  #=> nil
    

    注意return 关键字不是必需的(在原始代码中也不需要)。使用散列来表示要执行的操作类型似乎很奇怪。调用该方法会更有意义:

    calculate(6,2,3,:add)                #=> 11 
    calculate(6,2,3)                     #=> 11 
    calculate(6,2,3,:subtract)           #=> 1 
    

    我们可以这样实现:

    def calculate(*arguments)
      operation =
      case arguments.last
      when :add
        arguments.pop
        :add
      when :subtract
        arguments.pop
        :subtract
      else
        :add
      end
    
      case operation
      when :add
        add *arguments
      else
        subtract *arguments
      end
    end
    

    更好:

    def calculate(*arguments, op=:add)
      case op
      when :subtract
        subtract *arguments
      else
        add *arguments
      end
    end
    
    calculate(6,2,3,:add)                #=> 11 
    calculate(6,2,3)                     #=> 11 
    calculate(6,2,3,:subtract)           #=> 1 
    

    我对你提出的“永远感激不尽”感到不知所措,但如果你在几分钟内感谢我的努力,那就足够了。

    【讨论】:

    • 天哪,我猜是你,在我滚动了两个屏幕的答案之后:)
    • 哇。每次我脑子里冒出一个问题,你都会在下一行回答。谢谢您,先生,非常感谢您的帮助。
    • 因为它的calculate(*arguments),我们知道参数的数量是可变的,从代码中我们看到最后一个参数可能是一个哈希。如果它是一个哈希,它告诉我们是否应该添加或减去前面的参数;如果不是哈希,我们将添加所有参数。
    【解决方案2】:

    current_result = numbers.shift。为什么会在那里?我的意思是,我理解它的作用,但它在这段特定的代码中的作用是什么?

    该行从numbers 数组中删除第一个元素并将其分配给current_result。然后,为剩余的numbers 数组中的每个number 计算current_result - number

    日志记录示例:

    numbers = [20, 3, 2]
    current_result = numbers.shift
    
    current_result #=> 20
    numbers        #=> [3, 2]
    
    numbers.inject(current_result) do |current_result, number|
      (current_result - number).tap do |result|
        puts "#{current_result} - #{number} = #{result}"
      end
    end
    

    输出:

    20 - 3 = 17
    17 - 2 = 15
    

    15 将是方法的返回值。

    但是,不需要删除第一个元素; inject 默认这样做:

    如果你没有为memo显式指定一个initial值,那么collection的第一个元素被用作memo的初始值.

    因此,subtract 方法可以简化:

    def subtract(*numbers)
      numbers.inject { |current_result, number| current_result - number }  
    end
    
    subtract(20, 3, 2) #=> 15
    

    你也可以提供方法符号:

    def subtract(*numbers)
      numbers.inject(:-)
    end
    

    同样适用于add

    def add(*numbers)
      numbers.inject(:+)
    end
    

    【讨论】:

    • 感谢您指出 shift 在此实现中完全没有必要。
    【解决方案3】:

    subtract方法说明

    假设您的输入是 [2,3,4,5]。

    在你的脑海中,你可以认为你需要一个可以做到这一点的程序

    result = 2 # This is first element of array
    result = result - 3  # This is second element of array
    result = result - 4  # This is third element of array
    result = result - 5  # This is fourth element of array
    

    所以最后你会得到

    result = -10
    

    shift 允许您从数组中取出第一个元素,以便您可以在剩余元素上使用inject,并将数组的第一个元素移位作为accumulator 值。

    如果您知道inject 方法works 的方式,上述解释将是有意义的。

    add 方法也可以采用相同的方法。

    def add(*numbers)
      current_result = numbers.shift
      numbers.inject(current_result) { |current_result, number| current_result + number }  
    end
    

    但是,add 代码的作者在他们的实现中使用了更正统的inject 版本,因为将0 添加到另一个数字不会产生任何影响。

    如果作者使用他们的add 方法并将+ 替换为-,他们最终会得到

    def subtract(*numbers)
      numbers.inject(0) { |result, number| result - number }  
    end
    

    但是,subtract([2,3,4,5]) 将提供-14 (0-2-3-4-5 = -14) 的输出,而不是-10 (2-3-4-5 = -10)。

    要修复上面的代码,必须去掉addsubtract 中的默认参数0,并编写如下代码

    def subtract(*numbers)
      numbers.inject { |result, number| result - number }  
    end
    
    def add(*numbers)
      numbers.inject { |result, number| result + number }  
    end
    

    我们现在将得到-10subtract([2,3,4,5]) 的正确结果

    上面的代码之所以有效,是因为如文档所述 here (并且也在其他答案中表示)

    注入的参数实际上是可选的。如果默认值不是 作为参数传入,第一次执行块时,第一次 参数将设置为可枚举的第一个元素和 第二个参数将设置为可枚举的第二个元素。

    总之,Rubymonk 的作者本可以避免这种混淆,或者他们可能试图通过使用 shift 来教给读者一个 Ruby 技巧

    calculate方法说明

    calculate 的易于理解的版本如下所示:

    def calculate(*arguments, options)
      return add(*arguments) if options[:add]
      return subtract(*arguments) if options[:subtract]
    end
    
    p calculate(1,2,3,4,{add: true}) # Adds the numbers, outputs 10
    p calculate(1,2,3,4,{subtract: true}) # Subtracts the numbers, outputs -8
    

    Ruby 会将函数调用的最后一个参数分配给options,并将其余元素作为数组分配给arguments。如果我们只用一个参数调用calculate,那将是options 的值。

    p calculate({add: true})  # Outputs 0
    p calculate(1) # Will return error as 1 is not a Hash and can't be assigned to 'options'
    

    代码作者希望将options 设为可选参数,这样下面的代码也能正常工作。

    p calculate(1,2,3,4) # Assume add, and output 10
    

    这将需要更改calculate 的简单版本,这样我们就可以将arguments 数组的最后一个元素用作options,而不是显式声明options 作为calculate 的参数。类型为Hash。因此,我们将最终实现calculate,如Rubymonk 所示。

    【讨论】:

    • 虽然你是正确的,但 shift 在这里没有用处,因为 inject 将自动使用第一个参数作为初始备忘录值,除非给出一个,所以在这种情况下 numbers.inject(:-) 将具有相同的影响或没有班次。
    • @engineersmnky 感谢您的提示。我最初的目标是回答 OP 中的问题,因为作者寻求对特定代码的解释
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-10-09
    • 1970-01-01
    • 2011-03-12
    • 1970-01-01
    相关资源
    最近更新 更多