奇怪的,意想不到的行为(消失/改变值),当使用哈希默认值,例如Hash.new()
考虑这个代码:
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]}
但远非如此。 发生了什么,我怎样才能得到我期望的行为?
首先,请注意,此行为适用于随后发生变化的任何默认值(例如哈希和string),而不仅仅是数组。
TL; DR :使用Hash.new { |h, k| h[k] = [] }
Hash.new { |h, k| h[k] = [] }
如果你想要最简单,最习惯的解决scheme。
什么都行不通
为什么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 #=> {}
我们可以看到,我们的默认对象被重用和变异(这是因为它作为唯一的默认值传递,哈希没有办法得到一个新的,新的默认值),但为什么没有键或值在arrays中,尽pipeh[1]
仍然给我们一个价值? 这里有个提示:
h[42] #=> ["a", "b"]
每个[]
调用返回的数组只是默认值,我们一直在进行变异,所以现在包含了我们的新值。 由于<<
没有分配给散列(在没有=
存在的情况下不能在Ruby中赋值),所以我们从来没有把任何东西放到我们的实际散列中。 相反,我们必须使用<<=
(这是<<
as +=
is to +
):
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 { [] }
是一个可行的解决scheme,但它有点奇怪和非惯用(我从来没有见过<<=
在野外使用)。 如果<<
无意中使用,它也容易发生微妙的错误。
可变的方式
Hash.new
状态的文档 (强调我自己的):
如果指定了一个块,它将被用哈希对象和键调用,并且应该返回默认值。 如果需要,块的责任是将值存储在散列中 。
因此,如果我们希望使用<<
而不是<<=
那么我们必须在块中存储散列中的默认值:
h = Hash.new { |h, k| h[k] = [] } h[0] << 'a' #=> ["a"] h[1] << 'b' #=> ["b"] h #=> {0=>["a"], 1=>["b"]}
这有效地将来自我们的个别调用(将使用<<=
)的赋值移动到传递给Hash.new
的块,消除了在使用<<
时出现意外行为的负担。
请注意,此方法与其他方法之间有一个function差异:这种方式在读取时分配默认值(因为分配总是在块内发生)。 例如:
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"]}
在所有的方法中,我个人更喜欢这种方式 – 不变性通常会使事情的推理更为简单(毕竟,这是唯一不存在隐藏或微妙的意外行为的方法)。
†这不是严格正确的,像instance_variable_set
这样的方法绕过了这个方法,但是它们必须存在于元编程中,因为l中的值不能是dynamic的。
当您调用Hash.new([])
,任何键的默认值不只是一个空数组,它是同一个空数组。
要为每个默认值创build一个新数组,请使用构造函数的块forms:
Hash.new { [] }
您指定哈希的默认值是对特定(最初为空)数组的引用。
我想你想要:
h = Hash.new { |hash, key| hash[key] = []; } h[1]<<=1 h[2]<<=2
这将每个键的默认值设置为一个新的数组。
应用于这些散列的运算符+=
按预期工作。
[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]=[];}
之间没有区别 Hash.new{|hash, key| hash[key]=[];}
。 至less在ruby上2.1.2。
当你写的时候,
h = Hash.new([])
您将数组的默认引用传递给散列中的所有元素。 因为哈希中的所有元素都指向相同的数组。
如果你想要散列中的每个元素指向单独的数组,你应该使用
h = Hash.new{[]}
有关如何在ruby中工作的更多细节请通过这个: http : //ruby-doc.org/core-2.2.0/Array.html#method-c-new