在Rails代码中,人们倾向于使用Enumerable #injection方法来创建哈希,如下所示:
somme_enum.inject({}) do |hash, element|
hash[element.foo] = element.bar
hash
end
虽然这似乎已经成为一种常见的习惯用法,但是有没有人看到优于“天真”版本的优势,这将是:
hash = {}
some_enum.each { |element| hash[element.foo] = element.bar }
我在第一个版本中看到的唯一优势是你在一个封闭的块中执行它并且你没有(显式地)初始化哈希。否则它会以一种意想不到的方式滥用方法,难以理解并且难以阅读。那为什么它如此受欢迎?
答案 0 :(得分:30)
正如Aleksey所指出的,Hash#update()比Hash#store()慢,但这让我想到了#inject()与直接#each循环的整体效率。所以我对一些事情进行了基准测试:
(注意:2012年9月19日更新,包括#each_with_object)
(注意:2014年3月31日更新,包括#by_initialization,感谢https://stackoverflow.com/users/244969/pablo的建议)
require 'benchmark'
module HashInject
extend self
PAIRS = 1000.times.map {|i| [sprintf("s%05d",i).to_sym, i]}
def inject_store
PAIRS.inject({}) {|hash, sym, val| hash[sym] = val ; hash }
end
def inject_update
PAIRS.inject({}) {|hash, sym, val| hash.update(val => hash) }
end
def each_store
hash = {}
PAIRS.each {|sym, val| hash[sym] = val }
hash
end
def each_update
hash = {}
PAIRS.each {|sym, val| hash.update(val => hash) }
hash
end
def each_with_object_store
PAIRS.each_with_object({}) {|pair, hash| hash[pair[0]] = pair[1]}
end
def each_with_object_update
PAIRS.each_with_object({}) {|pair, hash| hash.update(pair[0] => pair[1])}
end
def by_initialization
Hash[PAIRS]
end
def tap_store
{}.tap {|hash| PAIRS.each {|sym, val| hash[sym] = val}}
end
def tap_update
{}.tap {|hash| PAIRS.each {|sym, val| hash.update(sym => val)}}
end
N = 10000
Benchmark.bmbm do |x|
x.report("inject_store") { N.times { inject_store }}
x.report("inject_update") { N.times { inject_update }}
x.report("each_store") { N.times {each_store }}
x.report("each_update") { N.times {each_update }}
x.report("each_with_object_store") { N.times {each_with_object_store }}
x.report("each_with_object_update") { N.times {each_with_object_update }}
x.report("by_initialization") { N.times {by_initialization}}
x.report("tap_store") { N.times {tap_store }}
x.report("tap_update") { N.times {tap_update }}
end
end
Rehearsal -----------------------------------------------------------
inject_store 10.510000 0.120000 10.630000 ( 10.659169)
inject_update 8.490000 0.190000 8.680000 ( 8.696176)
each_store 4.290000 0.110000 4.400000 ( 4.414936)
each_update 12.800000 0.340000 13.140000 ( 13.188187)
each_with_object_store 5.250000 0.110000 5.360000 ( 5.369417)
each_with_object_update 13.770000 0.340000 14.110000 ( 14.166009)
by_initialization 3.040000 0.110000 3.150000 ( 3.166201)
tap_store 4.470000 0.110000 4.580000 ( 4.594880)
tap_update 12.750000 0.340000 13.090000 ( 13.114379)
------------------------------------------------- total: 77.140000sec
user system total real
inject_store 10.540000 0.110000 10.650000 ( 10.674739)
inject_update 8.620000 0.190000 8.810000 ( 8.826045)
each_store 4.610000 0.110000 4.720000 ( 4.732155)
each_update 12.630000 0.330000 12.960000 ( 13.016104)
each_with_object_store 5.220000 0.110000 5.330000 ( 5.338678)
each_with_object_update 13.730000 0.340000 14.070000 ( 14.102297)
by_initialization 3.010000 0.100000 3.110000 ( 3.123804)
tap_store 4.430000 0.110000 4.540000 ( 4.552919)
tap_update 12.850000 0.330000 13.180000 ( 13.217637)
=> true
Enumerable#each比Enumerable #inject更快,而Hash #store比Hash #renew更快。但最快的是在初始化时传递一个数组:
Hash[PAIRS]
如果您在创建哈希后添加元素,则获胜版本正是OP建议的那样:
hash = {}
PAIRS.each {|sym, val| hash[sym] = val }
hash
但是在这种情况下,如果你是一个想要单一词汇形式的纯粹主义者,你可以使用#tap和#each并获得相同的速度:
{}.tap {|hash| PAIRS.each {|sym, val| hash[sym] = val}}
对于那些不熟悉tap的人,它会在主体内部创建一个接收器(新哈希)的绑定,最后返回接收者(相同的哈希)。如果您了解Lisp,请将其视为Ruby的LET绑定版本。
-whew-。谢谢你的聆听。
由于人们已经问过,这里是测试环境:
# Ruby version ruby 2.0.0p247 (2013-06-27) [x86_64-darwin12.4.0]
# OS Mac OS X 10.9.2
# Processor/RAM 2.6GHz Intel Core i7 / 8GB 1067 MHz DDR3
答案 1 :(得分:22)
美丽是旁观者的眼睛。具有一些函数式编程背景的人可能更喜欢基于inject
的方法(就像我一样),因为它具有与fold
higher-order function相同的语义,这是从多个计算单个结果的常用方法投入。如果您了解inject
,那么您应该了解该功能正在按预期使用。
作为这种方法看起来更好的一个原因(在我看来),请考虑hash
变量的词法范围。在基于inject
的方法中,hash
仅存在于块的主体内。在基于each
的方法中,块内的hash
变量需要与块外部定义的某些执行上下文一致。想在同一个函数中定义另一个哈希?使用inject
方法,可以剪切并粘贴基于inject
的代码并直接使用它,并且几乎肯定不会引入错误(忽略是否应该使用C& P)编辑 - 人们这样做。使用each
方法,您需要C& P代码,并将hash
变量重命名为您想要使用的任何名称 - 额外步骤意味着这更容易出错。
答案 2 :(得分:10)
inject
(又名reduce
)在函数式编程语言中有着悠久而受人尊敬的地位。如果您准备冒险尝试并希望了解Matz对Ruby的大量启发,那么您应该阅读http://mitpress.mit.edu/sicp/在线提供的开创性的计算机程序结构和解释。
一些程序员发现在一个词法包中包含所有内容,风格更清晰。在哈希示例中,使用注入意味着您不必在单独的语句中创建空哈希。更重要的是,inject语句直接返回结果 - 您不必记住它在哈希变量中。为了清楚地说明这一点,请考虑:
[1, 2, 3, 5, 8].inject(:+)
VS
total = 0
[1, 2, 3, 5, 8].each {|x| total += x}
第一个版本返回总和。第二个版本将总和存储在total
中,作为程序员,您必须记住使用total
而不是.each
语句返回的值。
一个小小的附录(纯粹是自然的 - 不是关于注入):你的例子可能写得更好:
some_enum.inject({}) {|hash, element| hash.update(element.foo => element.bar) }
...因为hash.update()
返回哈希本身,所以最后不需要额外的hash
语句。
@Aleksey让我感到羞愧,因为他对各种组合进行了基准测试。请参阅我在其他地方的基准回复。简短形式:
hash = {}
some_enum.each {|x| hash[x.foo] = x.bar}
hash
是最快的,但可以稍微优雅地重铸 - 而且速度一样快: -
{}.tap {|hash| some_enum.each {|x| hash[x.foo] = x.bar}}
答案 3 :(得分:3)
我刚刚找到了
Ruby inject with initial being a hash
建议使用each_with_object
代替inject
:
hash = some_enum.each_with_object({}) do |element, h|
h[element.foo] = element.bar
end
对我来说似乎很自然。
另一种方法,使用tap
:
hash = {}.tap do |h|
some_enum.each do |element|
h[element.foo] = element.bar
end
end
答案 4 :(得分:2)
如果你要返回一个哈希,使用merge可以保持它更干净,所以你不必在之后返回哈希。
some_enum.inject({}){|h,e| h.merge(e.foo => e.bar) }
如果你的枚举是一个哈希,你可以很好地得到键和值(k,v)。
some_hash.inject({}){|h,(k,v)| h.merge(k => do_something(v)) }
答案 5 :(得分:1)
我认为这与不完全了解何时使用reduce的人有关。我同意你的观点,每一种方式都应该是