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(引数)という書き方が、その引数を1回だけ評価してできたオブジェクトを「唯一のデフォルト値」として保存する仕組みだからです。存在しないキーへのアクセスのたびに毎回その同じオブジェクトを返すため、複数のキーが裏で同じ実体を共有します。ブロック形式のHash.new { |h, k| h[k] = [] }は、存在しないキーへのアクセスのたびにブロックの中身を毎回新しく実行するため、キーごとに独立した新しい配列が作られます。Pythonの可変なデフォルト引数が1回しか評価されない問題と、根本的な原因は同じです。