传递给 Hash.new 的块或对象何时创建或运行?

3 ruby hash

我正在阅读 ruby​​ koans,但在理解何时运行此代码时遇到了一些麻烦:

hash = Hash.new {|hash, key| hash[key] = [] }
Run Code Online (Sandbox Code Playgroud)

如果散列中没有值,新数组何时分配给散列中的给定键?第一次访问哈希值而不先分配它时会发生这种情况吗?请帮助我了解何时为任何给定的哈希键创建了确切的默认值。

Car*_*and 7

为了那些 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 kh[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)

这消除了两行代码。

哪个最好?

就我个人而言,我对第二种和第三种解决方案无动于衷。两者都在实践中使用。