3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

RubyAdvent Calendar 2022

Day 23

Hash.new([]) の挙動にハマった

Posted at

はじめに

Hash.new([]) を使用して処理を書く場面があったのですが、
その際の思わぬ挙動にハマった話とその対処方法について記事にしました

問題のコード

processed_ids = ::Hash.new {[]}

# user からは group_id と user_id を参照できる
users.each do |user|
    next if processed_ids[user.group_id].include?(user.user_id)

    processed_ids[user.group_id].push(user.user_id)
end

processed_ids にキーとして group_id を指定し、それに対して配列で user_id を追加していき、
既に group_id に対応する配列に user_id が追加されていたら、処理をスキップする、という処理を書いています。

上記のような記述で動作する...と思っていたのですが、
実際に動かしてみると想定した通りに動きません。

実際にコンソールで処理を細かく見ていくと、以下のような形になっていました。

irb(main):001:0> processed_ids = ::Hash.new([])
=> {}
irb(main):002:0> processed_ids[1].push(101)
=> [101]
irb(main):003:0> processed_ids[1]
=> [101]
irb(main):004:0> processed_ids[2]
=> [101] # !?
irb(main):005:0> processed_ids[2].include?(101)
=> true # !!?

Hash に対して新しくキーを指定し push で値を追加してから他のキーで参照すると、
そのキーでも同じ値が格納されていました。
そんなバカなと思い、ドキュメントを確認してみると...

  • new(ifnone = nil) -> Hash
    • 空の新しいハッシュを生成します。ifnone はキーに対応する値が存在しない時のデフォルト値です。設定したデフォルト値はHash#defaultで参照できます。
    • ifnoneを省略した Hash.new は {} と同じです。
    • デフォルト値として、毎回同一のオブジェクトifnoneを返します。それにより、一箇所のデフォルト値の変更が他の値のデフォルト値にも影響します。

一箇所のデフォルト値の変更が他の値のデフォルト値にも影響します。

!?
どうやらこの挙動は仕様して存在しているようです。
ドキュメントに例として以下のコードが書かれていましたが、

h = Hash.new([])

p h[1]                  #=> []
p h[1].object_id        #=> 6127150
p h[1] << "bar"         #=> ["bar"]
p h[1]                  #=> ["bar"]

p h[2]                  #=> ["bar"]
p h[2].object_id        #=> 6127150

p h                     #=> {}

h[1] に格納した値と、h[2]に格納されている値が同じものになっています。
なるほど...

対処方法

以下のように Hash の宣言をすると、先に挙げた挙動を回避できました。

Hash.new { |hash, key| hash[key] = [] }

初期化の際にブロックを与えると、
まだ値が無いキーを参照した際にそのブロックが評価され別のオブジェクトになってくれるようです。

irb(main):001:0> processed_ids = Hash.new { |hash, key| hash[key] = [] }
=> {}
irb(main):002:0> processed_ids[1].push(101)
=> [101]
irb(main):003:0> processed_ids[1]
=> [101]
irb(main):004:0> processed_ids[2]
=> []
irb(main):005:0> processed_ids[2].include?(101)
=> false
3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?