首先,请注意,此行为适用于随后发生变化的任何默认值(例如哈希和字符串),而不仅仅是array.它也同样适用于Array.new(3) { [] }
中填充的元素.
TL;DR:如果你想要最惯用的解决方案,并且不在乎为什么,就使用Hash.new { |h, k| h[k] = [] }
.
What doesn’t work
Why Hash.new([])
doesn’t work
让我们更深入地了解为什么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中,如果没有=
present†,就永远不会有分配),因此我们从未在实际的散列中放入任何内容.相反,我们必须使用<<=
(即<<=
到<<
,正如+=
到+
):
h[2] <<= 'c' #=> ["a", "b", "c"]
h #=> {2=>["a", "b", "c"]}
这与:
h[2] = (h[2] << 'c')
Why Hash.new { [] }
doesn’t work
使用Hash.new { [] }
可以解决重用和修改原始默认值的问题(每次调用给定的块时,都会返回一个新数组),但不能解决赋值问题:
h = Hash.new { [] }
h[0] << 'a' #=> ["a"]
h[1] <<= 'b' #=> ["b"]
h #=> {1=>["b"]}
What does work
作业(job)方式
如果我们记得总是使用<<=
,那么Hash.new { [] }
is是一个可行的解决方案,但它有点奇怪和不习惯(我从未见过<<=
在野外使用).如果无意中使用<<
,它也容易出现细微的错误.
易变的方式
documentation for Hash.new
个州(我自己的州):
如果指定了一个块,将使用哈希对象和键调用它,并应返回默认值.It is the block’s responsibility to store the value in the hash if required
因此,如果我们希望使用<<
而不是<<=
,我们必须在块内的散列中存储默认值:
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
.因此,如果您想确保始终保持不变性,那么必须注意重新冻结新对象.
Conclusion
在所有这些方法中,我个人更喜欢"不可变的方法"——不可变通常会使事情的推理变得更简单.毕竟,这是唯一一种不可能出现隐藏或微妙意外行为的方法.然而,最常见和惯用的方式是"可变方式".
最后,散列默认值的这种行为在Ruby Koans中有所说明.
†严格来说这不是真的,像instance_variable_set
这样的方法绕过了这一点,但它们必须存在于元编程中,因为=
中的l值不能是动态的