[解決済み] Hash.new([]などの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}
それはそれでいいんだけどさ。
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]}
と表示されますが、それとはかけ離れています。何が起きているのか、どうすれば期待通りの動作を得られるのか。
どのように解決するのですか?
まず、この動作は配列だけでなく、その後に変更されるあらゆるデフォルト値(ハッシュや文字列など)にも適用されることに注意してください。また、これは
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
の状態です (強調は私自身)。
ブロックが指定された場合、ハッシュオブジェクトとキーで呼び出され、デフォルト値を返すはずです。 必要であれば、ハッシュに値を保存するのはブロックの責任です。 .
を使いたい場合は、ブロック内からハッシュにデフォルト値を格納する必要があるわけです。
<<
の代わりに
<<=
:
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のNumericsはimmutableなので、当然ながらインプレースで変異させることはない、ということです。もしデフォルト値をimmutableとして扱えば
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 の公案 .
†
これは厳密には正しくなく、以下のようなメソッドがあります。
instance_variable_set
のようなメソッドはこれをバイパスしますが、これらはメタプログラミングのために存在しなければなりません。
=
のl値は動的であってはならないからです。
関連
-
[解決済み】Rubyでハッシュのすべての値を変更する
-
[解決済み] Rubyの文字列の中から"˶‾‾‾˵"を削除するにはどうしたらいいですか?
-
[解決済み] Rubyにおけるtapメソッドの利点
-
[解決済み] Rubyで空のファイルを作成する:"touch "と同等?
-
[解決済み] rubyのClassとKlassの違いは何ですか?
-
[解決済み] Ruby: 文字列の最初の文字を取得する方法
-
[解決済み] Herokuの基本的なアプリのロードに2秒かかるのはなぜですか?
-
[解決済み] Capybaraで要素の正確なテキストをマッチングして要素を見つける方法
-
[解決済み] Rubyで、selectとmapを組み合わせたArrayメソッドはありますか?
-
[解決済み] Rubyの基本的なプロジェクトはどのように設定するのですか?
最新
-
nginxです。[emerg] 0.0.0.0:80 への bind() に失敗しました (98: アドレスは既に使用中です)
-
htmlページでギリシャ文字を使うには
-
ピュアhtml+cssでの要素読み込み効果
-
純粋なhtml + cssで五輪を実現するサンプルコード
-
ナビゲーションバー・ドロップダウンメニューのHTML+CSSサンプルコード
-
タイピング効果を実現するピュアhtml+css
-
htmlの選択ボックスのプレースホルダー作成に関する質問
-
html css3 伸縮しない 画像表示効果
-
トップナビゲーションバーメニュー作成用HTML+CSS
-
html+css 実装 サイバーパンク風ボタン
おすすめ
-
[解決済み] なぜsumはinject(:+)よりもずっと速いのですか?
-
[解決済み] ...』の違い (ダブルドット) と '...' の違い?(トリプルドット)の違いについて教えてください。
-
[解決済み] Rubyの理想的なプロジェクト構造
-
[解決済み] 文字列を正規表現に変換する ruby
-
[解決済み] Rubyでコンソールから入力を読み込む?
-
[解決済み] Ruby: HTTP でファイルを multipart/form-data で投稿するには?
-
[解決済み] 2つのハッシュを比較するにはどうすればよいですか?
-
[解決済み] Ruby文字列のgsubメソッドとsubメソッドの違いについて
-
[解決済み] Ruby の文字列から最後の n 文字を抽出する。
-
[解決済み] 変数名を使ったRubyの正規表現