首先,请注意,此行为适用于随后发生变化的任何默认值(例如哈希和字符串),而不仅仅是数组。它也同样适用于 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 { [] }
是 一个可行的解决方案,但它有点奇怪且不符合惯用语(我从未见过 <<=
在野外使用)。如果 <<
不小心使用 ,它也容易出现微妙的错误。
可变的方式
Hash.new 的文档 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([])
不起作用而 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
。因此,如果您想确保始终保持不变性,则必须小心重新冻结新对象。
结论
在所有方法中,我个人更喜欢“不可变方法”——不可变性通常使推理变得简单得多。毕竟,它是唯一一种不可能出现隐藏或微妙的意外行为的方法。然而,最常见和最惯用的方法还是“可变方法”。
最后,Hash 默认值的这种行为在 Ruby Koans .
†这并不完全正确,像instance_variable_set
这样的方法会绕过这个问题,但它们必须存在于元编程中,因为=
中的左值不能是动态的。