Hash.new([]) のデフォルト値は全キーで同じ配列を共有してしまう
Hash.new([])で作ったハッシュは、存在しないキーにアクセスするたびに同じ1つの配列オブジェクトを返します。その配列を<<で変更すると、まだ触っていないはずの他のキーの値まで変わってしまいます。
なぜエラーが出ないのか
出力: [1, 2]
(エラーなし)- Rubyは何も報告しません。文法として正しいためです
出力: [1, 2]- 実際の挙動 — 期待した結果と食い違っている箇所
見つけ方- エラーが出ないので、出力を目で確かめるしかありません。この種の誤りが最も発見が遅れます
このエラーが出る典型パターン
パターン1
1 totals = Hash.new([]) ^ 2 totals[:a] << 1 3 totals[:b] << 2 4 puts totals[:a].inspect
Hash.new([])のデフォルト値は全キーで同じ配列オブジェクトです。:bへの追加が:aの中身にまで現れてしまいます。
直し方: Hash.new([]) を Hash.new { |h, k| h[k] = [] } にします。
パターン2
1 baskets = Hash.new([]) ^ 2 baskets[:red] << "apple" 3 baskets[:blue] << "grape" 4 puts baskets[:red].inspect
キーごとに独立した配列がほしい場合は、ブロック形式でアクセスのたびに新しい配列を作る必要があります。
直し方: Hash.new([]) を Hash.new { |h, k| h[k] = [] } にします。
パターン3
1 logs = Hash.new([]) ^ 2 logs[:info] << "start" 3 logs[:error] << "fail" 4 puts logs[:info].inspect
Hash.newの引数は一度だけ評価される1つのオブジェクトです。全キーがそれを使い回してしまいます。
直し方: Hash.new([]) を Hash.new { |h, k| h[k] = [] } にします。
パターン4
1 groups = Hash.new([]) ^ 2 groups[:a] << 1 3 groups[:b] << 2 4 puts groups[:a].inspect
配列の中身が意図せず混ざるバグは気づきにくく、キーを分けたつもりが同じ配列を共有していることが原因です。
直し方: Hash.new([]) を Hash.new { |h, k| h[k] = [] } にします。
パターン5
1 buckets = Hash.new([]) ^ 2 buckets[:x] << "a" 3 buckets[:y] << "b" 4 puts buckets[:x].inspect
ブロック形式のHash.newは、キーごとに個別のオブジェクトを生成するので共有バグを避けられます。
直し方: Hash.new([]) を Hash.new { |h, k| h[k] = [] } にします。
よくある誤解
「存在しないキーにアクセスするたびに新しい空配列が用意されるはず」という思い込みは誤りです。Hash.newに渡した引数はただ1つのオブジェクトとして生成され、全キーで使い回されます。キーごとに独立させたい場合はブロック形式 Hash.new { |h, k| h[k] = [] } を使う必要があります。
まとめ
Hash.new([]) のデフォルト値は全キーで同じ配列を共有してしまうは中級でつまずきやすい項目です。上の5パターンを実際に手で直すと、エラーメッセージのどこを読めばよいかが掴めます。