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