考虑这个代码:

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

但事实远非如此.发生了什么,我如何才能达到预期的行为?

推荐答案

首先,请注意,此行为适用于随后发生变化的任何默认值(例如哈希和字符串),而不仅仅是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值不能是动态的

Ruby相关问答推荐

Ruby插值法导致无序输出

Ruby 3 从多个预定纤程中收集结果

有没有办法在 Capybara 中保持登录状态?

Ruby注入daisy链?

Ruby 匹配第一次出现的字符串以进行 gsub 替换

bundler 可以告诉我 Gemfile 中的哪些 gem 有更新的版本

Ruby 中的字符串是可变的吗?

如何用 Ruby 覆盖 shell 中的打印行?

从Electron邮件中删除签名和回复

为什么在 ruby​​ / rails / activerecord 中并不总是需要 self ?

`respond_to?` 与 `respond_to_missing?`

发现 Ruby 对象成员?

从 Ruby 中的 URL 获取文件名的好方法是什么?

Ruby 将标题发布到 slug

为什么显式返回会对 Proc 产生影响?

如何将多个元素添加到数组中?

在类方法中使用实例变量 - Ruby

在 Ruby 中获取用户主目录的跨平台方法?

Ruby:捕获异常后继续循环

Ruby 的 %q / %Q 引用方法的用例是什么?