Ruby 用 inject 和 each_with_object 來組 hash


在本期Ruby Weekly上有一篇很有趣的討論,關於在Ruby當中組成Hash的方法。

inject是用來處理陣列的方法,例如數列加總:

array = [1,2,3,4,5]
array.inject(0) {|result, number| result + number}
# => 15

首先,inject會帶入我們設定的變數(0)當做初始值,而這個初始值就是block中的第一個變數(result),接下來帶入數列中的數值到第二個變數(number),最後將回傳值儲存到result,重新再跑一遍。最後回傳result給使用者。

針對整理Hash的三種解法

整理資料庫時,也會使用inject整理,一般來說是為了方便查找資料,例如在Rails當中先把user的名字和電話先對起來:

User.all.inject({}) do |result, user|
    result[user[:name]] = user[:phone]
    result
end

這樣一來,我們就有一個hash可以直接用名字來查詢電話號碼。

但在這樣的組合方法中,有一個很不符合Ruby風格的地方,就是在第3行的地方,因為inject每一次重跑時會取用前一次的回傳值當做result,因此最後需要使用result變數回傳,否則依照Ruby回傳值的邏輯會產生錯誤。

如果要把User這段code用一行解決,可以選擇用merge:

User.all.inject({}) do |result, user|
    result.merge(user[:name] => user[:phone])
end

不過對於原本文章作者Chris Mar而言,這依然不夠直觀,因為他喜歡原本直接用等號(=)的方式,因此他想到了另一個解法,使用each_with_object

User.all.each_with_object({}) |user, result|
    result[user[:name]] = user[:phone]
end

這裡和inject有幾個不同的地方: 1. 所帶的變數順序與inject相反,要累積的result會放在後方 2. 每一次跑完不取回傳值,而是取原本的result值,不需擔心回傳值的問題 3. 只能使用於object、hash、array等型態,無法用於數字和字串。例如:

[1,2,3].each_with_object(0) {|number, result| result + number}
# => 0
# => 永遠都是原始值,並不會改變

以上總共提到三種解法。

三種解法的效能問題

不過另一位網友Andy Croll從效能的角度來討論,merge的速度是最快的。他使用了Benchmark ips這個gem,可以將不同的code丟進去檢查效能,並分別在幾個方法上丟入1000筆資料,得出以下結果。

第一種:inject方法         => 3136.7 i/s
第二種:merge方法          => 5.9 i/s
第三種:each_with_object  => 3220.8 i/s

這...也太打臉的吧,前一篇文章整理出來最好的方法是eachwithobject,但效能卻是最差的,反而是中途使用的merge方法最簡單。筆者認為其實測量單位非常的小,即便以上幾個方法有著600多倍的差距,但實際在程式或網站上執行的速度,應該跟前端讀取和資料庫讀取比起來,還是快如閃電。各位覺得呢?

延伸閱讀

三種解法

三種解法的效能