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

ep6jt1vc  于 2022-12-29  发布在  Ruby
关注(0)|答案(4)|浏览(135)

请看下面的代码:

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

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

pwuypxnk

pwuypxnk1#

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

    • 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 { [] } * 是 * 一个可行的解决方案,但是它有点奇怪并且不习惯(我从来没有见过<<=被广泛使用),如果<<被不经意地使用,它也很容易产生微妙的bug。

多变的方式

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中的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。因此,如果要确保始终保持不变性,则必须小心地重新冻结新对象。

结论

在所有的方法中,我个人更喜欢"不变的方法"--不变通常会使对事物的推理简单得多。毕竟,它是唯一没有隐藏或微妙的意外行为可能性的方法。然而,最常见、最惯用的方法是"可变的方法"。
最后一点,Ruby Koans中提到了Hash默认值的这种行为。
†严格来说这不是真的,像instance_variable_set这样的方法绕过了这一点,但是它们必须存在于元编程中,因为=中的l值不能是动态的。

llmtgqce

llmtgqce2#

指定哈希的默认值是对该特定(初始为空)数组的引用。
我认为你想要:

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

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

ars1skjm

ars1skjm3#

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

[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]被求值时,它返回 default value 对象,并且+操作符不会改变它。左手是[]=方法的语法糖,它不会改变 default value
请注意,这不适用于foo[bar]<<=baz,因为它将等效于foo[bar]=foo[bar]<<baz,并且<<更改 * 默认值 *。
而且,我发现Hash.new{[]}Hash.new{|hash, key| hash[key]=[];}之间没有区别,至少在ruby 2.1.2上是这样。

okxuctiv

okxuctiv4#

当你写作的时候,

h = Hash.new([])

你把数组的默认引用传递给散列中的所有元素.因为散列中的所有元素引用相同的数组.
如果希望哈希中每个元素引用单独的数组,则应该使用

h = Hash.new{[]}

要了解更多关于ruby的细节,请浏览:http://ruby-doc.org/core-2.2.0/Array.html#method-c-new

相关问题