首先,请注意,此行为适用于随后突变的任何默认值(例如,哈希和字符串),而不仅是数组。
TL; DR:Hash.new { |h, k| h[k] = [] }
如果您想要最惯用的解决方案,而不管为什么,请使用。
什么不起作用
为什么Hash.new([])
不起作用
让我们更深入地研究为什么Hash.new([])
不起作用:
h = Hash.new([])
h[0] << 'a' #=> ["a"]
h[1] << 'b' #=> ["a", "b"]
h[1] #=> ["a", "b"]
h[0].object_id == h[1].object_id #=> true
h #=> {}
我们可以看到默认对象正在被重用和变异(这是因为它作为唯一的默认值传递,散列无法获取新的默认值),但是为什么没有键或值在数组中,尽管h[1]
仍然给我们带来了价值?这里有一个提示:
h[42] #=> ["a", "b"]
每次[]
调用返回的数组只是默认值,我们一直在进行此操作,因此现在包含新值。由于<<
不分配到哈希(永远不会有没有Ruby的分配=
目前†),我们从来没有把任何东西到我们的实际哈希值。相反,我们必须使用<<=
(按<<
原样+=
使用+
):
h[2] <<= 'c' #=> ["a", "b", "c"]
h #=> {2=>["a", "b", "c"]}
这与以下内容相同:
h[2] = (h[2] << 'c')
为什么Hash.new { [] }
不起作用
using Hash.new { [] }
解决了重用和更改原始默认值的问题(因为每次调用给定的块,并返回一个新数组),但是没有分配问题:
h = Hash.new { [] }
h[0] << 'a' #=> ["a"]
h[1] <<= 'b' #=> ["b"]
h #=> {1=>["b"]}
什么有效
分配方式
如果我们记得始终使用<<=
,那么这Hash.new { [] }
是一个可行的解决方案,但这有点古怪且没有习惯性(我从未见过<<=
在野外使用过)。如果<<
不小心使用它,还容易产生细微的错误。
可变方式
在对文档Hash.new
状态(强调我自己的):
如果指定了块,则将使用哈希对象和键调用该块,并应返回默认值。如果需要,将值存储在哈希中是块的责任。
因此,如果我们希望使用默认值<<
代替,则必须将默认值存储在块内的哈希中<<=
:
h = Hash.new { |h, k| h[k] = [] }
h[0] << 'a' #=> ["a"]
h[1] << 'b' #=> ["b"]
h #=> {0=>["a"], 1=>["b"]}
这有效地将分配从我们的单个调用(将使用<<=
)转移到传递给的块Hash.new
,从而消除了使用时意外行为的负担<<
。
请注意,此方法与其他方法有一个功能上的区别:这种方式在读取时分配默认值(因为分配始终在块内进行)。例如:
h1 = Hash.new { |h, k| h[k] = [] }
h1[:x]
h1 #=> {:x=>[]}
h2 = Hash.new { [] }
h2[:x]
h2 #=> {}
不变的方式
您可能想知道为什么Hash.new([])
在正常工作时不起作用Hash.new(0)
。关键是Ruby中的数字是不可变的,因此我们自然不会最终就地改变它们。如果我们将默认值视为不可变的,则也可以使用以下方法Hash.new([])
:
h = Hash.new([].freeze)
h[0] += ['a'] #=> ["a"]
h[1] += ['b'] #=> ["b"]
h[2] #=> []
h #=> {0=>["a"], 1=>["b"]}
但是,请注意([].freeze + [].freeze).frozen? == false
。因此,如果要确保始终保留不变性,则必须注意重新冻结新对象。
结论
在所有方式中,我个人更喜欢“不变的方式” —不变性通常使事情的推理变得简单得多。毕竟,这是唯一不可能隐藏或微妙的意外行为的方法。但是,最常见和惯用的方式是“可变方式”。
最后,在Ruby Koans中记录了Hash默认值的这种行为。
†严格地说,这不是正确的方法,例如instance_variable_set
绕过此方法,但它们必须存在以进行元编程,因为in的l值=
不能是动态的。