为什么 RuboCop 建议将 .times.map 替换为 Array.new?

Why does RuboCop suggest replacing .times.map with Array.new?

RuboCop 建议:

Use Array.new with a block instead of .times.map.

docs 中为警察:

This cop checks for .times.map calls. In most cases such calls can be replaced with an explicit array creation.

示例:

# bad
9.times.map do |i|
  i.to_s
end

# good
Array.new(9) do |i|
  i.to_s
end

我知道可以替换,但我感觉9.times.map更接近英文语法,更容易理解代码的作用

为什么要更换?

后者性能更高;这是一个解释:Pull request where this cop was added

It checks for calls like this:

9.times.map { |i| f(i) }
9.times.collect(&foo)

and suggests using this instead:

Array.new(9) { |i| f(i) }
Array.new(9, &foo)

The new code has approximately the same size, but uses fewer method calls, consumes less memory, works a tiny bit faster and in my opinion is more readable.

I've seen many occurrences of times.{map,collect} in different well-known projects: Rails, GitLab, Rubocop and several closed-source apps.

Benchmarks:

Benchmark.ips do |x|
  x.report('times.map') { 5.times.map{} }
  x.report('Array.new') { Array.new(5){} }
  x.compare!
end
__END__
Calculating -------------------------------------
           times.map    21.188k i/100ms
           Array.new    30.449k i/100ms
-------------------------------------------------
           times.map    311.613k (± 3.5%) i/s -      1.568M
           Array.new    590.374k (± 1.2%) i/s -      2.954M

Comparison:
           Array.new:   590373.6 i/s
           times.map:   311612.8 i/s - 1.89x slower

I'm not sure now that Lint is the correct namespace for the cop. Let me know if I should move it to Performance.

Also I didn't implement autocorrection because it can potentially break existing code, e.g. if someone has Fixnum#times method redefined to do something fancy. Applying autocorrection would break their code.

如果觉得可读性更好,就采纳吧

这是一个性能规则,您应用程序中的大多数代码路径可能对性能都不是关键的。就个人而言,我总是倾向于支持可读性而不是过早的优化。

也就是说

100.times.map { ... }
  • times 创建一个 Enumerator 对象
  • map 在无法优化的情况下枚举该对象,例如,预先不知道数组的大小,它可能必须动态地重新分配更多 space 并且它必须枚举通过调用 Enumerable#each 的值,因为 map 是以这种方式实现的

鉴于

Array.new(100) { ... }
  • new 分配大小为 N 的数组
  • 然后使用本机循环填充值

当您需要映射调用固定次数的块的结果时,您可以选择:

Array.new(n) { ... }

和:

n.times.map { ... }

后者对于 n = 10 大约慢 60%,对于 n > 1_000 下降到大约 40%。

注:对数刻度!