Ruby 注意使用 inject 和 reduce

pathbox · 2017年08月24日 · 最后由 adamshen 回复于 2017年08月24日 · 2551 次阅读

最近在使用 inject 的时候遇到了一个问题。我的代码例子是:

[1,2,3,4,5,6].inject([]) do |sum, i|
  if i > 1
    sum << i
  end
end

结果报错:

NoMethodError: undefined method `<<' for nil:NilClass

一开始很奇怪,在 inject([]) 的时候,我给 sum 赋值初始值为数组了,报错信息怎么会报 sum 是一个 nil:NilClass 呢?

打断点

def test_inject
  [1,2,3,4,5,6].inject([]) do |sum, i|
    if i > 1
      binding.pry
      sum << i
    end
  end
end

发现确实 sum 是 nil。想到了可能是 if i > 1 这个条件判断的影响。修改一下测试代码

def test_inject_0
  [1,2,3,4,5,6].inject([]) do |sum, i|
    if i > 0
      sum << i
    end
  end
end

def test_inject_3
  [4,5,6,1,2,3].inject([]) do |sum, i|
    binding.pry
    if i > 3
      sum << i
    end
  end
end
test_inject_0  # [1, 2, 3, 4, 5, 6]

test_inject_3  
sum
# []
# [4]
# [4,5]
# [4,5,6]
# NoMethodError: undefined method `<<' for nil:NilClass

根据 test_inject_3,当 i 的值不满足 if i > 3 的条件的时候,sum 的值就被置为 nil 了。

如果你需要在 inject 的 block 中进行对操作元素的条件过滤操作,你会将 sum 变为 nil。

所以建议不在 inject 的 block 中进行条件过滤操作,而是在 inject 方法之前过滤好。

同理 reduce 方法。

Ruby 版本:ruby 2.4.0p0 (2016-12-24 revision 57164) [x86_64-linux]

当我没说。

@pathbox

https://apidock.com/ruby/Enumerable/inject

参考 inject 使用文档

[1,2,3,4,5,6].inject([]) do |sum, i|  

当你对数组调用 inject 方法并进行迭代时,sum 刚开始是为 [],然后下面一句是需要注意的,迭代过程中 block 的返回值会赋值给 sum,并作为下一次的输入,最终迭代完成后,sum 的值作为返回结果。

In either case, the result becomes the new value for memo. At the end of the iteration, the final value of memo is the return value for the method.

所以,当进行如下判断时

if i > 1 
  sum <<  i
end

执行完成 if 语句后,返回的值为 nil,此时本次迭代的返回值 nil 就会赋值给 sum,当下一次再进行迭代时,就会报你遇到的以下错误

NoMethodError: undefined method `<<' for nil:NilClass

解决办法

在 block 迭代的时候,显式返回 sum 就行了

[1,2,3,4,5,6].inject([]) do |sum, i|
  if i > 1
    sum << i
  end
  sum
end

可以用 each_with_object,这样可以少写一行

需要 登录 后方可回复, 如果你还没有账号请 注册新账号