使用Hash默认值(例如Hash.new([]))时出现奇怪的意外行为(值消失/更改)


107

考虑以下代码:

h = Hash.new(0)  # New hash pairs will by default have 0 as values
h[1] += 1  #=> {1=>1}
h[2] += 2  #=> {2=>2}

很好,但是:

h = Hash.new([])  # Empty array as default value
h[1] <<= 1  #=> {1=>[1]}                  ← Ok
h[2] <<= 2  #=> {1=>[1,2], 2=>[1,2]}      ← Why did `1` change?
h[3] << 3   #=> {1=>[1,2,3], 2=>[1,2,3]}  ← Where is `3`?

在这一点上,我希望哈希为:

{1=>[1], 2=>[2], 3=>[3]}

但这远非如此。发生了什么事,我如何得到预期的行为?

Answers:


164

首先,请注意,此行为适用于随后突变的任何默认值(例如,哈希和字符串),而不仅是数组。

TL; DRHash.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值=不能是动态的。


1
值得一提的是,使用“可变方式”还具有使每个哈希查找存储键值对的效果(因为在该块中发生了赋值),这可能并非总是如此。
johncip

@johncip并非每次查找,只是每个键的第一个。但是我明白你的意思了,我稍后再将其添加到答案中。谢谢!。
Andrew Marshall

哎呀,马虎。您是对的,当然,这是对未知密钥的第一次查找。我几乎觉得自己{ [] }<<=有惊喜最少,如果不是事实,不小心忘记了=可能会导致一个非常混乱的调试会话。
johncip

关于使用默认值初始化哈希值时的差异的非常清晰的解释
cisolarix

23

您要指定哈希的默认值是对该特定(最初为空)数组的引用。

我想你要:

h = Hash.new { |hash, key| hash[key] = []; }
h[1]<<=1 
h[2]<<=2 

这会将每个键的默认值设置为一个数组。


如何为每个新哈希使用单独的数组实例?
Valentin Vasilyev

5
该块版本为您提供Array了每次调用时的新实例。智慧:h = Hash.new { |hash, key| hash[key] = []; puts hash[key].object_id }; h[1] # => 16348490; h[2] # => 16346570。另外:如果您使用的是设置值({|hash,key| hash[key] = []})的块版本,而不是仅生成值({ [] })的块版本,则在添加元素时只需要<<,而无需<<=
James A. Rosen

3

+=应用于这些哈希的运算符将按预期工作。

[1] pry(main)> foo = Hash.new( [] )
=> {}
[2] pry(main)> foo[1]+=[1]
=> [1]
[3] pry(main)> foo[2]+=[2]
=> [2]
[4] pry(main)> foo
=> {1=>[1], 2=>[2]}
[5] pry(main)> bar = Hash.new { [] }
=> {}
[6] pry(main)> bar[1]+=[1]
=> [1]
[7] pry(main)> bar[2]+=[2]
=> [2]
[8] pry(main)> bar
=> {1=>[1], 2=>[2]}

这可能是因为foo[bar]+=baz是句法糖foo[bar]=foo[bar]+baz时,foo[bar]在右手=被评为则返回默认值对象和+操作不会改变它。该[]=方法的左手是语法糖,不会更改默认值

请注意,这并不适用,foo[bar]<<=baz因为它等同于foo[bar]=foo[bar]<<baz并且<< 更改默认值

另外,我发现Hash.new{[]}和之间没有区别Hash.new{|hash, key| hash[key]=[];}。至少在红宝石2.1.2上。


很好的解释。似乎在ruby 2.1.1 Hash.new{[]}上与Hash.new([])我一样,但缺少预期的<<行为(尽管可以正常Hash.new{|hash, key| hash[key]=[];}工作)。奇怪的小事情打破了所有事情:/
butterywombat 2014年

1

当你写的时候

h = Hash.new([])

您将数组的默认引用传递给哈希中的所有元素。因为散列中的所有元素都引用相同的数组。

如果您希望哈希中的每个元素都引用单独的数组,则应使用

h = Hash.new{[]} 

有关它在ruby中如何工作的更多详细信息,请阅读以下内容:http : //ruby-doc.org/core-2.2.0/Array.html#method-c-new


这是错误的,Hash.new { [] }不能正常工作。有关详细信息,请参见我的答案。它也已经是另一个答案中提出的解决方案。
安德鲁·马歇尔
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.