Posted at

Rubyでネストしたhashの未定義の要素にnilを返させる方法まとめ

More than 1 year has passed since last update.

ネストしたhashで未定義の要素を取得した場合、親要素から未定義の場合エラーになってしまう。たとえば下のコードでは、hash_2[:b]nilを返してしまうので、hash_2[:b][:b]を取得すると#<NoMethodError: undefined method '[]' for nil:NilClass>のようなエラーが返ってきます。これを回避し、 hash_2[:b][:b]nilを返すようにする方法をまとめました。

hash_2 = { a: {a: "A"} }

p "a.aを取り出す"
a = hash_2[:a][:a] # Aが出力される
p a

p "a.bを取り出す"
a = hash_2[:a][:b] # nilが出力される
p a

p "b.bを取り出す"
begin
b = hash_2[:b][:b] # エラーになるから何とかしたい
p b
rescue => e
p e
end


実行環境

以下の二つのバージョンでの動作確認を行いました。rubyのバージョンによりできることが少し変わっています。


  • ruby 2.2.9p480 (2017-12-15 revision 61259) [x86_64-linux]

  • ruby 2.3.6p384 (2017-12-14 revision 61254) [x86_64-linux]


回避策


fetchを使う

Hashのfetchメソッドは引数にkeydefaultを指定し、keyが存在すればその値を、存在しなければ指定したdefaultを返します。これを使い、以下のようにhashの中間の要素にはdefaultに空のhash{}を、末端の要素にnilを指定します。

hash_2 = { a: {a: "A"} }

p "fetchを使う"
b = hash_2.fetch(:b, {}).fetch(:b, nil)
p b

コードが横に長くなるのが難点。


&&を使う

hash_2[:b] && hash_2[:b][:b]とします。この場合、hash_2[:b]が評価された時点でnilなのでそのままnilを返します(hash_2[:b][:b]は評価しないのでエラーにならない)。hash_2[:a] && hash_2[:a][:a]の場合は前から順番に評価されていきhash_2[:a][:a]が返されます。また&&の代わりにandにすると{:a=>"A"}を、つまりhash_2[:a] && hash_2[:a][:a]の先頭のhash_2[:a]を返してくるのでダメです。

hash_2 = { a: {a: "A"} }

p "&&を使う"
b = hash_2[:b] && hash_2[:b][:b]
p b

これもfetch同様、横に長くなりますね。


rescueを使う

hash_2[:b][:b]にアクセスしたときのエラーを後ろに置いたresqueで拾って、nilを返すようにします。

hash_2 = { a: {a: "A"} }

p "rescueを使う"
b = hash_2[:b][:b] rescue nil
p b

この書き方だとNoMethodError以外もnilを返してしまうのが気になりますが、ネストが何段でもコードが短く済むのと、空配列や0等任意の値を返すようにできるのは便利です。

また、{ b: "B" }のようにhash_2[:b]が定義されて、hash_2[:b][:b]が未定義でもnilになります(他の方法はだいたいエラーになる)。


mergeを使う

あらかじめ、全ての要素をnilであらかじめ定義したhashを別に作成し、mergeすることで、定義済みの要素はそのまま未定義の要素はnilになります。

hash_2 = { a: {a: "A"} }

p "mergeを使う"
nil_hash = {
a: {a: nil},
b: {b: nil}
}

merge_hash = nil_hash.merge(hash_2)
b = merge_hash[:b][:b]
p b

yamlやjsonをロードするとき等、形式は決まっている場合には、最初に一回mergeすれば、以後、nilを気にしないで良いのは便利だが、あまり使いたいものではないですね。


to_hを使う(2.0から?)

to_hはhashならそのままhashを、nilなら{}を返します。

hash_2 = { a: {a: "A"} }

p "to_hを使う(2.0から?)"
b = hash_2.2.0から[:b].to_h[:b]
p b

fetchよりは短いので2.0からはアリかも


&.を使う(2.3から)

ぼっち演算子を使うとobject が nil でないときにメソッドを呼びます。これで、[]メソッドを読んで取得します。

hash_2 = { a: {a: "A"} }

p "&.を使う"
b = hash_2&.[](:b)&.[](:b)
p b

長さ的にはto_hと変わらないのと2.3からは次のdigの方がよさそうです。


digを使う(2.3から)

hashのネストした要素にアクセスし未定義ならnilを返してくれます。

hash_2 = { a: {a: "A"} }

p "digを使う(2.3から)"
b = hash_2.dig(:b, :b)
p b

基本的にこれを使うべきみたいです。


まとめ

2.3以上ならdigが便利です。ただ、古いバージョンのRubyを使う場合は私はrescueを使う方法で回避しています。