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

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

Jim Rogers 1月前

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

返回
作者最近主题: