如何在Ruby中对数组内对象的属性求和

56

我知道在Ruby中,要对数组元素求和可以使用inject方法,例如:

array = [1,2,3,4,5];
puts array.inject(0, &:+) 

但是如何对对象数组中的属性进行求和呢?

假设有一个对象数组,每个对象都有一个"cash"属性。我想把它们的现金余额加起来得出总数。类似于以下代码...

array.cash.inject(0, &:+) # (but this doesn't work)

我意识到我可能可以创建一个仅由现金属性组成的新数组并对其求和,但如果可能的话,我正在寻找一种更清洁的方法!

7个回答

73
array.map(&:cash).inject(0, &:+)
或者
array.inject(0){|sum,e| sum + e.cash }

4
如果元素很多的话,这样做会使array被遍历两次,可能不是一个好主意。为什么不为inject使用一个适当的块呢?而且reduce/inject可以直接接受符号作为参数,不需要使用Symbol#to_proc :-) - Michael Kohl
请注意,您无需发送块,“inject”会知道如何处理符号:“inject(0,:+)” - tokland
3
Yuri,我点赞了你的回答,但第二个片段看起来不太好,最好使用一个功能性的代码:array.inject(0) { |sum, e| sum + e.cash } - tokland
我以为它可能是一个哈希表,我的错。 - Yuri Barbashov

63

3
如果您在使用Rails,这就是最佳选择。 - Dennis
请注意,如果您的数组是对ActiveRecord对象进行某种过滤的结果,例如 @orders = Order.all; @orders.select { |o| o.status == 'paid' }.sum(&:cost),那么您也可以通过查询获得相同的结果:@orders.where(status: :paid).sum(:cost) - Dennis
如果记录没有存储在数据库中,总和将为0,这时候inject会起作用。 - dgmora
6
更多关于@Dennis评论的内容:如果你正在使用Rails 4.1+,你不能在ActiveRecord relation上使用array.sum(&:cash),因为它会想要执行一个ActiveRecord sum,像这样:array.sum(:cash),这是有很大区别的(SQL与Ruby)。你需要将其转换成数组才能使其再次工作:array.to_a.sum(&:cash)。相当令人讨厌! - Augustin Riedinger
@AugustinRiedinger 如果可能的话,最好使用 SQL sum 而不是 Ruby sum,对吧? - Danny
这取决于使用情况:比如说你需要在页面上加载对象数组,进行Ruby计算将避免查询select sum(field) from table,但如果你只需要总和值,那么select sum(field) from table肯定比在Ruby中解析对象并进行求和的select * from table更快。 - Augustin Riedinger

11

#reduce 接受一个代码块(&:+ 是创建执行 + 的 Proc/代码块的快捷方式)。这是实现你所需功能的方法之一:

array.reduce(0) { |sum, obj| sum + obj.cash }

2
#reduce 在 1.9+ 中是 #inject 的别名,顺便说一下。 - Theo
+1 不要对 array 进行两次迭代。顺便说一下,这个别名在 1.8.7 中也存在。 - Michael Kohl
1
正如Michael所说,相比于map+reduce而言这种方法更加节省空间,但代价是牺牲了模块化(在这个案例中很小,无需赘述)。在Ruby 2.0中我们可以通过惰性求值实现两者兼得:array.lazy.map(&:cash).reduce(0, :+) - tokland
我想知道为什么会有这样的别名。它们的长度相同。 - Nerian
3
在Smalltalk中,inject:into:被称为折叠操作,而其他一些语言将其称为“reduce”(例如Clojure、Common Lisp、Perl和Python)。这些别名是为了适应具有不同编程背景的人。map/ collect也是如此。 - Michael Kohl

5

最简洁的方法:

array.map(&:cash).sum

如果map方法生成的数组中包含nil元素:
array.map(&:cash).compact.sum

3

如果求和的起始值为0,则sum函数与inject函数是相同的:

array.map(&:cash).sum

我更倾向于块级版本:

array.sum { |a| a.cash }

因为Proc符号通常过于有限(没有参数等)。(需要Active_Support)

2

以下是一些有趣的基准测试结果

array = Array.new(1000) { OpenStruct.new(property: rand(1000)) }

Benchmark.ips do |x|
  x.report('map.sum') { array.map(&:property).sum }
  x.report('inject(0)') { array.inject(0) { |sum, x| sum + x.property } }
  x.compare!
end

并显示结果

Calculating -------------------------------------
             map.sum   249.000  i/100ms
           inject(0)   268.000  i/100ms
-------------------------------------------------
             map.sum      2.947k (± 5.1%) i/s -     14.691k
           inject(0)      3.089k (± 5.4%) i/s -     15.544k

Comparison:
           inject(0):     3088.9 i/s
             map.sum:     2947.5 i/s - 1.05x slower

正如您所看到的,注入速度更快了一点


1

在注入时不需要使用initial,而且加法操作可以更短。

array.map(&:cash).inject(:+)

3
你关于符号参数的说法是正确的,但如果 array 可以为空,你需要使用参数:[].inject(:+) #=> nil[].inject(0, :+) #=> 0,除非你想单独处理 nil - Michael Kohl

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接