構造が定まってないHashにアクセスする場合は[]よりもfetchのほうが安全
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 だが、同じようなものとして扱えると思う