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

Val*_*yev 102 ruby hash

考虑以下代码:

h = Hash.new(0)  # New hash pairs will by default have 0 as values
h[1] += 1  #=> {1=>1}
h[2] += 2  #=> {2=>2}
Run Code Online (Sandbox Code Playgroud)

这一切都很好,但是:

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`?
Run Code Online (Sandbox Code Playgroud)

在这一点上,我希望哈希是:

{1=>[1], 2=>[2], 3=>[3]}
Run Code Online (Sandbox Code Playgroud)

但它远非如此.发生了什么,我怎样才能得到我期望的行为?

And*_*all 160

首先,请注意此行为适用于随后发生变异的任何默认值(例如哈希和字符串),而不仅仅是数组.

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  #=> {}
Run Code Online (Sandbox Code Playgroud)

我们可以看到我们的默认对象正在被重用和变异(这是因为它作为唯一的默认值传递,哈希无法获得新的默认值),但为什么没有键或值在阵列中,尽管h[1]仍然给我们一个价值?这是一个提示:

h[42]  #=> ["a", "b"]
Run Code Online (Sandbox Code Playgroud)

每次[]调用返回的数组都是默认值,我们一直在变异,所以现在包含我们的新值.既然<<没有分配给哈希(在没有=现有†的情况下永远不会在Ruby中进行赋值),我们从未在实际哈希中添加任何内容.相反,我们必须使用<<=(这是<<因为+=+):

h[2] <<= 'c'  #=> ["a", "b", "c"]
h             #=> {2=>["a", "b", "c"]}
Run Code Online (Sandbox Code Playgroud)

这与:

h[2] = (h[2] << 'c')
Run Code Online (Sandbox Code Playgroud)

为什么Hash.new { [] }不起作用

使用Hash.new { [] }解决了重用和改变原始默认值的问题(因为每次调用给定的块,返回一个新数组),但不是分配问题:

h = Hash.new { [] }
h[0] << 'a'   #=> ["a"]
h[1] <<= 'b'  #=> ["b"]
h             #=> {1=>["b"]}
Run Code Online (Sandbox Code Playgroud)

什么工作

分配方式

如果我们记得总是使用<<=,那么这Hash.new { [] } 一个可行的解决方案,但它有点奇怪和非惯用(我从未见过<<=用于野外).如果<<无意中使用它也容易出现细微的错误.

可变的方式

各州的文件Hash.new(强调我自己):

如果指定了一个块,它将使用哈希对象和键调用,并应返回默认值.如果需要,块负责将值存储在哈希中.

因此,如果我们希望使用<<而不是使用以下内容,我们必须将默认值存储在块中的哈希值中<<=:

h = Hash.new { |h, k| h[k] = [] }
h[0] << 'a'  #=> ["a"]
h[1] << 'b'  #=> ["b"]
h            #=> {0=>["a"], 1=>["b"]}
Run Code Online (Sandbox Code Playgroud)

这有效地将分配从我们的单个调用(将使用<<=)移动到传递给的块Hash.new,从而消除了使用时的意外行为负担<<.

请注意,此方法与其他方法之间存在一个功能差异:这种方式在读取时分配默认值(因为赋值始终发生在块内).例如:

h1 = Hash.new { |h, k| h[k] = [] }
h1[:x]
h1  #=> {:x=>[]}

h2 = Hash.new { [] }
h2[:x]
h2  #=> {}
Run Code Online (Sandbox Code Playgroud)

不可改变的方式

你可能想知道为什么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"]}
Run Code Online (Sandbox Code Playgroud)

但请注意([].freeze + [].freeze).frozen? == false.因此,如果要确保始终保持不变性,则必须注意重新冻结新对象.


结论

在所有方面,我个人更喜欢"不可改变的方式" - 可变性通常会使事情的推理变得更加简单.毕竟,这是唯一一种不存在隐藏或微妙意外行为的方法.然而,最常见和惯用的方式是"可变的方式".

最后,Ruby Koans中记录了Hash默认值的这种行为.


这不是严格意义上的,instance_variable_set绕过这个方法,但它们必须存在于元编程中,因为l值=不能是动态的.


Mat*_*hen 23

您指定散列的默认值是对该特定(最初为空)数组的引用.

我想你想要:

h = Hash.new { |hash, key| hash[key] = []; }
h[1]<<=1 
h[2]<<=2 
Run Code Online (Sandbox Code Playgroud)

这会将每个键的默认值设置为数组.

  • 该块版本在每次调用时为您提供新的"Array"实例.即:`h = Hash.new {| hash,key | hash [key] = []; puts hash [key] .object_id}; h [1]#=> 16348490; h [2]#=> 16346570`.另外:如果你使用*设置*值的块版本(`{| hash,key | hash [key] = []}`)而不是简单*生成*值的那个(`{[]}` ),那么你在添加元素时只需要`<<`,而不是`<< =`. (5认同)