TIL

Rubyの入れ子ハッシュの初期化を完全に理解した

Rubyのハッシュを入れ子にする際、初期化方法を理解できておらずハマったので完全に理解することにした。

完全に理解するために、いつものチェリー本を参考にさせて頂いた(ついこの間読んで読書感想投稿してたのに恥ずかしい)。

https://gihyo.jp/book/2017/978-4-7741-9397-7

おさらい

Rubyのハッシュオブジェクトを生成するときはハッシュリテラル{}を使うのが一般的。

h = {}
p h #=> {}

h[:maguro] = "ご期待ください"
p h #=> {:maguro=>"ご期待ください"}

この場合、ハッシュのvalueには初期値がないため、存在しないキーを指定するとnilが返る。

h = {}
p h[:saba] #=> nil

存在しないキーを指定した時に初期値が返るようにするには、Hash.newの引数に初期値を指定する。競プロでよく使うのが、キーに対して何らかのカウント結果を保持するケース。初期値に0を設定しておくとh[key] += 1でカウントができる。

h = Hash.new("初期値だよ")
p h[:saba] #=> "初期値だよ"

h2 = Hash.new(0)
3.times { h2[:hoge] += 1 }
p h2 #=> {:hoge=>3}

初期値への破壊的変更

ハッシュオブジェクトに設置された初期値は、Hash#defaultで確認できる。

h = Hash.new("hello")
puts h.default #=> hello

ここで、ハッシュの初期値に破壊的変更を加えてみる。

h = Hash.new("hello")
puts h.default #=> hello

h[:hoge].upcase!
puts h[:fuga]  #=> HELLO
puts h.default #=> HELLO

見ての通り、h[:hoge]の値を破壊的なupcase!で大文字に変えるとハッシュの初期値自体が書き換わってしまう。

なんとなく「それはそう」と思う。

入れ子ハッシュ初期化の罠

ここで、入れ子のハッシュ、つまり多重ハッシュを作りたい場合を考える。

h = Hash.new({})
p h[:hoge] #=> {} 

なんとなくこんな感じで初期化するで良さそうに思ってしまうが、いけない。

内側のハッシュオブジェクトで存在しないキーにアクセスすると初期値を参照し、そこに値を追加すると初期値が書き換わってしまう。つまり内側はすべて同じハッシュオブジェクトを見ることになる。

h = Hash.new({})
h[:hoge][:a] = 100
p h[:hoge] #=> {:a=>100}
p h[:fuga] #=> {:a=>100}

ハッシュ生成するブロックをHash.newに渡す

この問題を回避して、内側のハッシュオブジェクトをすべて別のオブジェクトとするにはこうする。

h = Hash.new { |h,k| h[k] = {} }
h[:hoge][:a] = 100
p h[:hoge] #=> {:a=>100}
p h[:fuga] #=> {}

何をやっているかはさておき、期待通りの動きになっている。

このとき、hのデフォルト値はどうなっているかを見てみる。

h = Hash.new { |h,k| h[k] = {} }
p h.default #=> nil

ハッシュオブジェクトが返ると思いきやnilになっている。

Hash.newにブロックを渡した場合はデフォルト値の保持方法が異なり、ブロックを元に生成されたProcオブジェクトが保持されるようだ。初期Procオブジェクト(?)はHash#default_procで確認できる。

h = Hash.new { |h,k| h[k] = {} }
p h.default_proc #=> #<Proc:0x00007f897b960328 hashtest.rb:1>

ハッシュに存在しないキーが指定される度にこのProcオブジェクトのcallが実行されて、空のハッシュが生成されるんだろう(Rubyの実装までは読みに行ってない)。

一応、こんなことをする必要は全く無いが、Procオブジェクトを予め作ってからブロックとして渡してもHash#default_procに設定される。

init_proc = Proc.new { |h,k| h[k] = {} }
h = Hash.new(&init_proc)
p h.default_proc #=> #<Proc:0x00007f82981444f8 hashtest.rb:1>

リファレンスマニュアルだとこのあたりに書いてある。

https://docs.ruby-lang.org/ja/3.0/class/Hash.html#S_NEW

空の新しいハッシュを生成します。ブロックの評価結果がデフォルト値になります。設定したデフォルト値はHash#default_procで参照できます。

値が設定されていないハッシュ要素を参照するとその都度ブロックを実行し、その結果を返します。ブロックにはそのハッシュとハッシュを参照したときのキーが渡されます。

まあだいたいわかったので、今日はこの辺にしといてやろう(震え声)。

一応仕事でRuby(といってもRailsばかりだけど)使ってるのにこのザマである。自分の浅さを日々感じるばかり。


<< 記事一覧へ