きっかけ
以下のようなコードをもっと手軽に書けないかなと思ったのが発端です。
hash = {}
hash[:yuno] = {}
hash[:yuno][:miyako] = {}
方法
@takeyuichi さんが素晴らしい書き方を教えて下さいました。感謝っ…! 圧倒的感謝っ…!
hash = Hash.new { |h, k| h[k] = h.dup.clear }
hash[:yuno][:miyako] #=> {}
hash #=> { :yuno => { :miyako => {} } }
解説
これは Hash のデフォルト値を巧みに利用したテクニックだと思います。
例えば
hash = Hash.new({})
では、ローカル変数 hash
のデフォルト値は {}
になりますが、
そのデフォルト値の Hash のデフォルト値は特に指定されていないため nil
となってしまいます。
hash = Hash.new({})
hash[:yuno] #=> {}
hash[:sae] #=> {}
hash[:yuno][:miyako] #=> nil
つまり、要件を満たすには
「デフォルト値としてハッシュを返す」ハッシュをデフォルト値とする
必要があるわけです (ややこしいですね…) 。
そこで Hash#dup が役立ちます。要は
hash = Hash.new { |h, k| h[k] = h.dup }
hash[:yuno][:miyako] #=> {}
hash #=> {:yuno=>{:miyako=>{}}}
のように Hash である自分自身 をデフォルト値とすればよいのです。
(Hash#dup はデフォルト値も引き継ぎます。)
なるほど からうろこ!!!
しかしこのままでは
hash = Hash.new { |h, k| h[k] = h.dup }
hash[:yuno][:juniors] = ['乃莉', 'なずな']
hash[:yuno][:miyako] #=> { :juniors => ["乃莉", "なずな"] }
hash #=> { :yuno => { :juniors => ["乃莉", "なずな"], :miyako => { :juniors => ["乃莉", "なずな"] } } }
このようにデフォルト値が汚染されて (上書きされて) しまいます。
なので
hash = Hash.new { |h, k| h[k] = h.dup.clear }
hash[:yuno][:juniors] = ['乃莉', 'なずな']
hash[:yuno][:miyako] #=> {}
hash #=> { :yuno => { :juniors => ["乃莉", "なずな"], :miyako => {} } }
と dup のあとで clear を呼んでハッシュの中身をクリアすることで、
デフォルト値の汚染を防ぐことができます。
(Hash#clear はデフォルト値の設定はクリアしません。)
ちょっとした要件がきっかけで、非常に勉強になりました。
おまけ
Hash のデフォルト値には注意すべきポイントもあります。
詳しくは ハッシュの安全な扱い方 (デフォルト値とその落とし穴について) にまとめています。