🐈

【bugs.ruby Advent Calender】Hash のキーを破壊的に変更するとキーが重複するバグ報告【20日目】

2024/12/20に公開

bugs.ruby Advent Calender 20日目の記事です。

これはなに

今年1年間通してみてきた bugs.ruby のチケットの中から気になったものを1つずつ取り上げていく Advent Calender です。
取り上げるチケットは基本的にこのブログで取り上げたものになります。
記事のまとめは ここを参照 してください。

[Bug #20880] Hash allows array-type key duplicates

次のようにキーの値を破壊的に変更した場合に Hash のキーが重複してしまうというバグ報告になります。

# Array をキーとして Hash に値を割り当てる
ary = [1, 2]
hash = { ary => "hoge" }

pp hash
# => {[1, 2]=>"hoge"}


# ary の値を書き換えた上でそれをキーとして値を割り当てる
ary << 3
hash[ary] = "foo"

# [1, 2, 3] のキーが2つある
pp hash
# => {[1, 2, 3]=>"hoge", [1, 2, 3]=>"foo"}

Hash のキーは基本的に重複しないはずなのでこれはぎょっとしますね。
しかし、これは想定した挙動になっています。

解説

まず、前提として次のようなコードの場合 keyhash で保持している key のオブジェクトは同じものを指しています。

key = [1, 2]
hash = { key => 1 }

# この2つのオブジェクトは同じものを指している
pp key.__id__               # => 60
pp hash.keys.first.__id__   # => 60

なので key の値を書き換えると hash の値も書き換わります。

key = [1, 2]
hash = { key => 1 }

# key を書き換えると hash のキーも変わる
key << 3
pp hash
# => {[1, 2, 3]=>1}

その上で hash のキーはハッシュ値を元にして保存されています。
なのでハッシュ値が変わると正しく値が参照できなくなります。
以下のコードだと key の配列の中身が変わることでハッシュ値も変わります。

key = [1, 2]
hash = { key => 1 }

pp key.hash  # => -1656669622002675970
pp hash[key] # => 1

# key の値が変わるとハッシュ値も変わるので hash から参照できなくなる
key << 3
pp key.hash  # => -72493266775540042
pp hash[key] # => nil

これらを踏まえると以下のような挙動になります。

key = [1, 2]
hash = { key => 1 }

# key のハッシュ値が変わったあとに hash にアクセスすると『新しいキー』として値が割り当てられる
key << 3
hash[key] = 2

# ただし、参照している key のオブジェクト自体は同じなので同じキーが複数あるようにみえる
pp hash
# => {[1, 2, 3]=>1, [1, 2, 3]=>2}

こんな感じで奇妙であるんですが結果的に Hash のキーが重複することになります。
こういう『キーのハッシュ値が変わった場合』には Hash#rehash で更新することができます。

key = [1, 2]
hash = { key => 1 }

key << 3

# これは参照できない
pp hash[key] # => nil

# #rehash 後は参照することができる
hash.rehash
pp hash[key] # => 1

参照: Hash#rehash (Ruby 3.3 リファレンスマニュアル)

ちなみに keyString の場合は参照しているオブジェクトは異なるので以下のような挙動になります。

key = "homu"
hash = { key => 1 }

# 破壊的な変更を行う
key.upcase!

# hash が保持しているオブジェクトは異なるのでキーには反映されない
pp hash
# => {"homu"=>1}

# ハッシュ値も異なるので別のキーとして割り当てられる
hash[key] = 2

pp hash
# => {"homu"=>1, "HOMU"=>2}

以上、 Hash の奇妙な動作でした。

関連

GitHubで編集を提案

Discussion