Strange, unexpected behavior (disappearing/changing values) when using Hash default value, e.g. Hash.new([])

Consider this code:

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

That’s all fine, but:

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`?

At this point I expect the hash to be:

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

but it’s far from that. What is happening and how can I get the behavior I expect?

18792 次浏览

您正在指定散列的默认值是对该特定(最初为空)数组的引用。

我觉得你想要:

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

That sets the default value for each key to a new array.

操作符 +=在应用到这些哈希表时按预期工作。

[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]被计算时,它返回 默认值对象,而 +操作符不会改变它。左手是 []=方法的句法糖,它不会改变 默认值

Note that this doesn't apply to foo[bar]<<=bazas it'll be equivalent to foo[bar]=foo[bar]<<baz and << 威尔 change the 默认值.

而且,我发现 Hash.new{[]}Hash.new{|hash, key| hash[key]=[];}之间没有区别,至少在 ruby 2.1.2上是这样。

首先,请注意,这种行为适用于随后发生变化的任何默认值(例如散列和字符串) ,而不仅仅是数组。它还类似地应用于 Array.new(3, [])中填充的元素。

TL;DR: Use Hash.new { |h, k| h[k] = [] } if you want the most idiomatic solution and don’t care why.


什么行不通

为什么 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  #=> {}

We can see that our default object is being reused and mutated (this is because it is passed as the one and only default value, the hash has no way of getting a fresh, new default value), but why are there no keys or values in the array, despite h[1] still giving us a value? Here’s a hint:

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 { [] } 是一个可行的解决方案,但它有点奇怪和不习惯(我从未见过 <<=在野外使用)。如果无意中使用了 <<,它也容易出现细微的错误。

易变的方式

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  #=> {}

The immutable way

您可能想知道为什么 Hash.new([])不工作,而 Hash.new(0)工作得很好。关键是 Ruby 中的 Numerics 是不可变的,所以我们自然不会就地对它们进行变异。如果我们将默认值视为不可变的,那么我们也可以很好地使用 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。因此,如果希望确保始终保持不变性,那么必须注意重新冻结新对象。


结论

在所有的方法中,我个人更喜欢“不变的方法”ーー不变性通常会使事情的推理变得更简单。毕竟,它是唯一不可能发生隐藏或微妙的意外行为的方法。然而,最常见和惯用的方法是“易变的方法”。

As a final aside, this behavior of Hash default values is noted in Ruby Koans.


这并不完全正确,像 instance_variable_set这样的方法绕过了这一点,但是它们必须存在于元编程中,因为 =中的 l 值不能是动态的。

你写作的时候,

h = Hash.new([])

将数组的默认引用传递给散列中的所有元素。因为散列中的所有元素都引用同一个数组。

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

h = Hash.new{[]}

如果想了解更多关于它在 Ruby 中如何工作的细节,请浏览以下内容: Http://ruby-doc.org/core-2.2.0/array.html#method-c-new