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

时间:2010-04-23 12:32:46

标签: ruby hash

考虑以下代码:

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]}

但它远非如此。发生了什么,我怎样才能得到我期望的行为?

4 个答案:

答案 0 :(得分:156)

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

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 { [] }不起作用

使用Hash.new { [] }解决了重用和改变原始默认值的问题(因为每次调用给定的块,返回一个新数组),但不是分配问题:

h = Hash.new { [] }
h[0] << 'a'   #=> ["a"]
h[1] <<= 'b'  #=> ["b"]
h             #=> {1=>["b"]}

什么工作

分配方式

如果我们记得总是使用<<=,那么Hash.new { [] } 是一个可行的解决方案,但它有点奇怪且非惯用(我从未见过{{ 1}}在野外使用)。如果无意中使用<<=,它也容易出现微妙的错误。

可变的方式

documentation for 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([])工作正常。关键是Ruby中的Numerics是不可变的,所以我们自然不会最终在原地改变它们。如果我们将默认值视为不可变,我们也可以使用Hash.new(0)

Hash.new([])

但请注意h = Hash.new([].freeze) h[0] += ['a'] #=> ["a"] h[1] += ['b'] #=> ["b"] h[2] #=> [] h #=> {0=>["a"], 1=>["b"]} 。因此,如果您想确保始终保持不变性,那么您必须注意重新冻结新对象。


结论

在所有方面,我个人更喜欢“不可改变的方式” - 可变性通常会使事情的推理变得更加简单。毕竟,这是唯一一种不存在隐藏或微妙意外行为的方法。然而,最常见和惯用的方式是“可变的方式”。

最后一点,Hash默认值的这种行为在Ruby Koans中注明。


这不是严格意义上的,像([].freeze + [].freeze).frozen? == false这样的方法会绕过这个,但它们必须存在才能进行元编程,因为instance_variable_set中的l值不能动态的。

答案 1 :(得分:23)

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

我想你想要:

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

将每个键的默认值设置为 new 数组。

答案 2 :(得分: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]+=bazfoo[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上。

答案 3 :(得分:1)

当你写作时,

h = Hash.new([])

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

如果你想让hash中的每个元素引用单独的数组,你应该使用

h = Hash.new{[]} 

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