在字符串数组中映射 nils 时如何避免 NoMethodError 异常?

How can one avoid a NoMethodError exception when mapping over nils in an array of strings?

问题

回应related question, @DanRio asked this follow-up question

If an element in the array is nil, using array.map!(&:upcase) gives a no method error on it. How would I get around this?

因为这超出了原始问题的范围,所以我代表他在这里发布。

代码

这是用户询问的代码片段:

array = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
array.map!(&:upcase)

问题

原题中,数组中的值都是String对象。但是,如果事先不知道数组的内容,那么当然有可能某些元素可能为 nil。由于 nil 不 #respond_to? :upcase,数组中的任何 nils 都会触发您看到的 NoMethodError 异常。

可能的解决方案

有多种方法可以解决这个问题,包括:

  1. 在映射之前使用 Array#compact 删除 nils。
  2. rescue明确处理异常。
  3. 使用Array#map! to invoke the method using Ruby's new (as of Ruby 2.3.0) safe navigation operator的块形式。

我将重点关注答案剩余部分的最后一项。

使用Ruby的安全导航运算符

哪个答案最好取决于上下文,但我建议在常见情况下使用安全导航运算符。除了发行说明,我在任何地方都找不到它的文档,但它的工作原理很像 Rails.

中的 Object#try 方法

您可以使用安全导航仅在响应它的对象上调用 #upcase,而不是直接使用 map! &:upcase 映射每个元素。例如:

array = ["Monday", "Tuesday", "Wednesday", "Thursday", nil, "Friday"]
array.map! { |e| e&.upcase }
#=> ["MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", nil, "FRIDAY"]

这是不同方法的基准:

require 'benchmark'

arr = 100_000.times.map { nil }
Benchmark.bm do |bm|
  bm.report("compact then upcase\n") do
    arr.compact.each &:upcase
  end
  bm.report("save nagivation then upcase\n") do
    arr.each { |y| y&.upcase }
  end
  bm.report("respond_to then upcase\n") do
    arr.each { |y| y.upcase if y.respond_to?(:upcase) }
  end
  bm.report("boolean check then upcase\n") do
    arr.each { |y| y && y.upcase }
  end
end

结果:

       user     system      total        real
compact then upcase
  0.000000   0.000000   0.000000 (  0.000912)
save nagivation then upcase
  0.020000   0.000000   0.020000 (  0.005030)
respond_to then upcase
  0.000000   0.000000   0.000000 (  0.009824)
boolean check then upcase
  0.000000   0.000000   0.000000 (  0.005254)

您可以看到 compact 是最快的,但它的结果与其他结果之间存在重要差异。它不会在结果中包含任何 nil 值,如果跳过某些项目,那么结果将是不同的长度。

所以我想说,如果你希望结果不保留 nil,那么使用紧凑型,否则使用布尔或安全导航,它们的性能大致相同。