为了那些刚接触 Ruby 的人的利益,我已经讨论了解决问题的替代方法,包括这个问题的实质内容。
任务
假设给你一个数组
arr = [[:dog, "fido"], [:car, "audi"], [:cat, "lucy"], [:dog, "diva"], [:cat, "bo"]]
并希望创建哈希
{ :dog=>["fido", "diva"], :car=>["audi"], :cat=>["lucy", "bo"] }
第一个解决方案
h = {}
arr.each do |k,v|
h[k] = [] unless h.key?(k)
h[k] << v
end
h #=> {:dog=>["fido", "diva"], :car=>["audi"], :cat=>["lucy", "bo"]}
这很简单。
第二种解决方案
更像Ruby的写法:
h = {}
arr.each { |k,v| (h[k] ||= []) << v }
h #=> {:dog=>["fido", "diva"], :car=>["audi"], :cat=>["lucy", "bo"]}
当 Ruby 看到 (h[k] ||= []) << v 时,她做的第一件事就是将其扩展为
(h[k] = h[k] || []) << v
如果h没有键k,h[k] #=> nil,那么表达式就变成了
(h[k] = nil || []) << v
变成了
(h[k] = []) << v
所以
h[k] #=> [v]
请注意,相等左侧的h[k] 使用Hash#[]= 方法,而右侧的h[k] 使用Hash#[]。
此解决方案要求没有一个哈希值等于 nil。
第三种解决方案
第三种方法是给散列一个默认值。如果哈希h 没有键k,则h[k] 返回默认值。默认值有两种类型。
将默认值作为参数传递给Hash::new
如果将空数组作为参数传递给Hash::new,则该值将成为默认值:
a = []
a.object_id
#=> 70339916855860
g = Hash.new(a)
#=> {}
g[k] 在h 没有密钥k 时返回[]。 (但是,散列并没有改变。)这个结构有重要的用途,但在这里不合适。要了解原因,假设我们写了
x = g[:cat] << "bo"
#=> ["bo"]
y = g[:dog] << "diva"
#=> ["bo", "diva"]
x #=> ["bo", "diva"]
这是因为:cat 和:dog 的值都设置为等于同一个对象,一个空数组。我们可以通过检查object_ids 看到这一点:
x.object_id
#=> 70339916855860
y.object_id
#=> 70339916855860
给Hash::new一个返回默认值的块
默认值的第二种形式是执行块计算。如果我们用块定义哈希:
h = Hash.new { |h,k| h[key] = [] }
那么如果h 没有键k,h[k] 将被设置为等于块返回的值,在这种情况下是一个空数组。请注意,块变量h 是新创建的空哈希。这让我们可以写
h = Hash.new { |h,k| h[k] = [] }
arr.each { |k,v| h[k] << v }
h #=> {:dog=>["fido", "diva"], :car=>["audi"], :cat=>["lucy", "bo"]}
由于传递给块的第一个元素是arr.first,因此块变量通过评估来赋值
k, v = arr.first
#=> [:dog, "fido"]
k #=> :dog
v #=> "fido"
因此块计算是
h[k] << v
#=> h[:dog] << "fido"
但由于h(还)没有键:dog,因此会触发块,将h[k]设置为等于[],然后该空数组会附加“fido”,因此
h #=> { :dog=>["fido"] }
同样,在arr 的下两个元素被传递到我们拥有的块之后
h #=> { :dog=>["fido"], :car=>["audi"], :cat=>["lucy"] }
当arr 的下一个(第四个)元素被传递给块时,我们评估
h[:dog] << "diva"
但现在h 确实有一个密钥,所以默认值不适用,我们最终得到
h #=> {:dog=>["fido", "diva"], :car=>["audi"], :cat=>["lucy"]}
arr 的最后一个元素处理类似。
注意,当使用带有块的 Hash::new 时,我们可以这样写:
h = Hash.new { launch_missiles("any time now") }
在这种情况下,h[k] 将被设置为等于launch_missiles 的返回值。换句话说,任何事情都可以在块中完成。
更像Ruby
最后,更像 Ruby 的写法
h = Hash.new { |h,k| h[k] = [] }
arr.each { |k,v| h[k] << v }
h #=> {:dog=>["fido", "diva"], :car=>["audi"], :cat=>["lucy", "bo"]}
是使用Enumerable#each_with_object:
arr.each_with_object(Hash.new { |h,k| h[k] = [] }) { |k,v| h[k] << v }
#=> {:dog=>["fido", "diva"], :car=>["audi"], :cat=>["lucy", "bo"]}
这样就省去了两行代码。
哪个最好?
就个人而言,我对第二种和第三种解决方案无动于衷。两者都在实践中使用。