代码之家  ›  专栏  ›  技术社区  ›  Valentin V

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

  •  96
  • Valentin V  · 技术社区  · 16 年前

    考虑这个代码:

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

    但事实远非如此。发生了什么,我如何才能得到我期望的行为?

    4 回复  |  直到 10 年前
        1
  •  168
  •   Andrew Marshall    7 年前

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

    热释光;博士 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"]
    

    [] call只是默认值,我们一直在修改它,所以现在包含了我们的新值。自 << 不赋值给散列(在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{[]} 是 <<= 在野外使用)。它也容易出现细微的错误,如果 << 是无意中使用的。

    易变的方式

    这个 documentation for Hash.new

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

    << 而不是 <<=

    h = Hash.new { |h, k| h[k] = [] }
    h[0] << 'a'  #=> ["a"]
    h[1] << 'b'  #=> ["b"]
    h            #=> {0=>["a"], 1=>["b"]}
    

    这有效地将分配从我们的单个调用中移开(这将使用 <<= 哈希。新建 ,消除了使用时意外行为的负担 << .

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

    不变的方式

    你可能想知道为什么 不工作,而 Hash.new(0) 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 . 因此,如果要确保始终保持不变性,则必须注意重新冻结新对象。


    结论

    最后,散列默认值的这种行为在 Ruby Koans


    严格来说这不是真的,像 instance_variable_set = 不能是动态的。

        2
  •  23
  •   Matthew Flaschen    16 年前

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

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

        3
  •  3
  •   Daniel Ribeiro Moreira    12 年前

    接线员 += 当应用到这些散列时,它们会按预期工作。

    [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上。

        4
  •  1
  •   Ganesh Sagare    10 年前

    当你写作的时候,

    h = Hash.new([])
    

    将数组的默认引用传递给哈希中的所有元素。因为散列中的所有元素都引用相同的数组。

    如果希望散列中的每个元素都引用单独的数组,则应该使用

    h = Hash.new{[]} 
    

    有关ruby中如何工作的更多详细信息,请参阅以下内容: http://ruby-doc.org/core-2.2.0/Array.html#method-c-new