Hash.new([])などのHashのデフォルト値を使用すると、奇妙な予期しない動作(値の消失/変更)


107

このコードを考えてみましょう:

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

しかし、それとはかけ離れています。何が起こっているのか、どうすれば期待する動作を得ることができますか?

回答:


164

まず、この動作は配列だけでなく、後で変更されるデフォルト値(ハッシュや文字列など)にも適用されることに注意してください。

TL; DRHash.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(強調は私自身):

ブロックが指定されている場合、ハッシュオブジェクトとキーを使用して呼び出され、デフォルト値を返す必要があります。必要に応じて、ハッシュに値を格納するのはブロックの責任です

したがって<<<<=次の代わりに使用したい場合は、ブロック内からハッシュにデフォルト値を格納する必要があります。

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

これにより、割り当てが(を使用する<<=)個々の呼び出しからに渡されるブロックに効果的に移動し、を使用するHash.new場合の予期しない動作の負担がなくなります<<

この方法と他の方法には機能的な違いが1つあることに注意してください。この方法では、読み取り時にデフォルト値が割り当てられます(割り当ては常にブロック内で行われるため)。例えば:

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。したがって、不変性が全体にわたって維持されるようにする場合は、新しいオブジェクトを再度フリーズするように注意する必要があります。


結論

すべての方法の中で、私は個人的には「不変の方法」を好みます。一般に、不変性は物事についての推論をはるかに簡単にします。結局のところ、これは、予期しない動作が隠されたり微妙に行われたりする可能性がない唯一の方法です。ただし、最も一般的で慣用的な方法は「変更可能な方法」です。

最後に、このハッシュのデフォルト値の動作はRuby Koansに記載されています。


これは厳密には真実ではありません。instance_variable_setバイパスのような方法ですが、のl値を=動的にすることはできないため、メタプログラミングのために存在する必要があります。


1
「変更可能な方法」を使用すると、すべてのハッシュルックアップにキーと値のペアが格納されるという効果もあります(ブロック内で割り当てが行われているため)。これは常に望ましいとは限りません。
johncip 2015年

@johncip すべてのルックアップではなく、各キーへの最初のルックアップのみ。しかし、私はあなたが何を意味するのかを理解しています。それを後で答えに追加します。ありがとう!
Andrew Marshall

おっと、ずさんな。もちろん、その通りです。これは、未知のキーの最初のルックアップです。私はほとんどのように感じ{ [] }<<=、それが誤って忘れするという事実のためではなかった、最も少ないの驚きを持っている=非常に混乱デバッグセッションにつながる可能性があります。
johncip 2015年

デフォルト値でハッシュを初期化するときの違いに関するかなり明確な説明
cisolarix

23

ハッシュのデフォルト値がその特定の(最初は空の)配列への参照であることを指定しています。

私はあなたが望むと思います:

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

これにより、各キーのデフォルト値が新しい配列に設定されます。


新しいハッシュごとに個別の配列インスタンスを使用するにはどうすればよいですか?
Valentin Vasilyev 2010

5
そのブロックバージョンはArray、呼び出しごとに新しいインスタンスを提供します。ウィットするには:h = Hash.new { |hash, key| hash[key] = []; puts hash[key].object_id }; h[1] # => 16348490; h[2] # => 16346570。また、値()を生成するだけではなく、値()を設定するブロックバージョンを使用する場合は、要素を追加する場合は必要ありません。{|hash,key| hash[key] = []}{ [] }<<<<=
James

3

+=これらのハッシュに演算子を適用すると、期待どおりに機能します。

[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では。


いい説明。ruby 2.1.1 Hash.new{[]}では、Hash.new([])私と同じように思われ<<ますが、期待どおりの動作がありません(もちろんHash.new{|hash, key| hash[key]=[];}動作します)。すべてのものを壊す奇妙な小さなもの:/
butterywombat

1

あなたが書くとき、

h = Hash.new([])

ハッシュのすべての要素に配列のデフォルト参照を渡します。そのため、ハッシュのすべての要素は同じ配列を参照します。

ハッシュの各要素が個別の配列を参照するようにしたい場合は、

h = Hash.new{[]} 

Rubyでどのように機能するかについての詳細は、こちらをご覧ください。http//ruby-doc.org/core-2.2.0/Array.html#method-c-new


これは間違っHash.new { [] }ます。機能しませ。詳細については、私の回答を参照してください。また、別の回答で提案されているソリューションでもあります。
Andrew Marshall
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.