为什么在此任务中使用Ruby的Array#shift?

时间:2015-07-27 18:00:18

标签: ruby

我正在浏览关于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

3 个答案:

答案 0 :(得分:4)

添加(*号)

让我们首先调用:

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首先初始化"备忘录" suminject的参数(如果在这里,它有一个),然后传递"接收器"的第一个元素。 [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)分配给备忘录({{1然后继续如上所述从接收者的第二个元素(sum)开始。正如所料,这产生了相同的答案:

2

那么为什么要包含零参数呢?通常我们会希望def add(*numbers) numbers.inject { |sum, number| sum + number } end add(1,2,3) #=> 6 (即add()返回零。我会留给您调查这两种add(*[]))形式的内容。您可以得出什么结论?

正如@Stefan在答案中指出的那样,你可以简单地说:

inject

这就是你通常看到它的方式。

但是,如果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 带有&#34; truthy&#34;值(除了:addfalse之外的任何内容,我们要添加;如果哈希的键nil带有&#34; truthy&#34;值(除了{{之外的任何值) 1}}或:subtract,我们要减去。如果最后一个元素不是哈希值(false),则假定为nil

注意:

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 

我对你的承诺感到不知所措,感谢他们,并且感激不尽,但是如果你感谢我的努力几分钟,那就足够了。

答案 1 :(得分:3)

  

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

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

记录示例:

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默认执行此操作:

  

如果您没有为备忘录明确指定初始值,那么集合的第一个元素将用作备忘录的初始值

因此,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

答案 2 :(得分:2)

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])会提供-140-2-3-4-5 = -14)而不是-102-3-4-5 = -10)的输出。

要修复上述代码,必须删除0add中的默认参数subtract,并编写如下内容

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

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

我们现在可以获得-10

subtract([2,3,4,5])的正确结果

上述代码有效,因为记录在案here(并在其他答案中也有表达)

  

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

总之,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版本,以便我们可以使用options的最后一个元素,而不是明确声明calculate作为arguments的参数。如果数组为options,则数组为Hash。因此,我们将最终实施Rubymonk上显示的calculate