構造が定まってないHashにアクセスする場合は[]よりもfetchのほうが安全

Railsの話。Hashは[]で値が取り出せる

pry(main)> p = {a: {b: :c}}
pry(main)> p[:a][:b]
 => :c

Hashの構造が定まってなくて、求めている構造のときのみ値を取得するケースを考える

例えば信用できない外部APIのレスポンスをパースしたり、ユーザーがPOSTするデータ*1をパースするケースが該当する。

[]だと発生する問題

pry(main)> p = {} #  p = {a: {b: :c}}を期待しているが実際は {} が来た
=> {}

pry(main)> p[:a][:b] # NoMethodError: undefined method `[]' for nil:NilClass 

p.try(:[], :a).try(:[], :b) # tryを使ってNoMethodErrorを回避する
=> nil 

これで良いかと思ったら以下のケースで問題が起きた

pry(main)> p = {a: 'b'} # こういうのが来た
=> {:a=>"b"}

pry(main)> p.try(:[], :a).try(:[], :b)
# TypeError: no implicit conversion of Symbol into Integer
# from /Users/user/.rbenv/versions/2.5.7/lib/ruby/gems/2.5.0/gems/activesupport-5.2.3/lib/active_support/core_ext/object/try.rb:19:in `[]'

何故TypeErrorが起きたか

pry(main)> p = {a: 'b'}
=> {:a=>"b"}

pry(main)> p.try(:[], :a)
=> "b"

なので 文字列に対する[]メソッドが呼ばれる。 ドキュメントを見る限りこのメソッドの引数にsymbolは期待されていないので、無理やりIntegerに変換しようとしてエラーが起きたのだと思われる。

解決策

fetchを使う。 fetchは文字列に対してメソッドが定義されてないので、tryによってnilが返却される

pry(main)> p = {a: 'b'}
=> {:a=>"b"}
pry(main)> p.fetch(:a).try(:fetch, :b)
=> nil

教訓

  • 構造が定まってないHashにアクセスする場合は[]よりもfetchのほうが安全かもしれない。
  • [] はいろんなクラスに定義されているメソッドなので、型が不定な場合に[]を使うことに気をつけた方が良い。

*1:正確にはユーザーがPOSTするデータはHashではなくActionController::Parameters だが、同じようなものとして扱えると思う