8wDlpd.png
8wDFp9.png
8wDEOx.png
8wDMfH.png
8wDKte.png

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

Jim Rogers 1月前

57 0

考虑以下代码:h = Hash.new(0) # 新的哈希对默认将具有 0 作为值h[1] += 1 #=> {1=>1}h[2] += 2 #=> {2=>2} 这些都很好,但是:h = 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]}

但事实并非如此。发生了什么事?我怎样才能获得我期望的行为?

帖子版权声明 1、本帖标题:使用 Hash 默认值时出现奇怪、意外的行为(消失/更改值),例如 Hash.new([])
    本站网址:http://xjnalaquan.com/
2、本网站的资源部分来源于网络,如有侵权,请联系站长进行删除处理。
3、会员发帖仅代表会员个人观点,并不代表本站赞同其观点和对其真实性负责。
4、本站一律禁止以任何方式发布或转载任何违法的相关信息,访客发现请向站长举报
5、站长邮箱:yeweds@126.com 除非注明,本帖由Jim Rogers在本站《ruby》版块原创发布, 转载请注明出处!
最新回复 (0)
  • 这是错误的,Hash.new { [] } 不起作用。有关详细信息,请参阅我的答案。这也是另一个答案中提出的解决方案。

  • 当你写作时,

    h = Hash.new([])
    

    您将数组的默认引用传递给哈希中的所有元素。因为哈希中的所有元素都引用同一个数组。

    如果你希望哈希中的每个元素引用单独的数组,你应该使用

    h = Hash.new{[]} 
    

    有关它在 ruby​​ 中的工作原理的更多详细信息,请参阅: http://ruby-doc.org/core-2.2.0/Array.html#method-c-new

  • 接受的答案很棒,但我还是有点困惑。所以我用字符串尝试了同样的事情,对我来说这更有意义。

    (这无疑是一个虚构的例子)

    h = Hash.new("foo")
    
    h[:a]
    => "foo"
    
    h[:b]
    => "foo"
    
    h[:a] << "l"
    
    h[:a]
    => "fool"
    
    h[:b]
    => "fool"
    
    h[:c]
    => "fool"
    

    Hash.new("") 默认的字符串或数组 Hash.new([]) 完全相同的对象不是一个新的字符串或数组。

  • 很好的解释。对我来说,在 ruby​​ 2.1.1 上,Hash.new{[]} 似乎与 Hash.new([]) 相同,只是缺少预期的 << 行为(当然 Hash.new{|hash, key| hash[key]=[];} 可以工作)。奇怪的小事情破坏了所有事情 :/

  • 运算符 += 应用于这些哈希值时,它会按预期工作。

    [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]=[];} 。至少在 ruby​​ 2.1.2 上是这样。

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

  • 您正在指定哈希的默认值是对该特定(最初为空)数组的引用。

    我想你想要:

    h = Hash.new { |hash, key| hash[key] = []; }
    h[1]<<=1 
    h[2]<<=2 
    

    将每个键的默认值设置为一个数组。

  • 哎呀,太马虎了。当然,你说得对,这是第一次查找未知键。我几乎觉得带有 <<= 的 { [] } 意外最少,如果不是因为不小心忘记 = 可能会导致非常混乱的调试会话。

  • @johncip 不是每次查找,只是每个键的第一个查找。但我明白你的意思,我稍后会将其添加到答案中;谢谢!。

  • 值得一提的是,使用“可变方式”也会导致每次哈希查找都存储一个键值对(因为在块中发生了分配),但这可能并不总是我们所希望的。

  • 首先,请注意,此行为适用于随后发生变化的任何默认值(例如哈希和字符串),而不仅仅是数组。它也同样适用于 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这样的方法会绕过这个问题,但它们必须存在于元编程中,因为=中的左值不能是动态的。

返回
作者最近主题: