How to avoid NoMethodError for missing elements in

2019-01-01 07:37发布

I'm looking for a good way to avoid checking for nil at each level in deeply nested hashes. For example:

name = params[:company][:owner][:name] if params[:company] && params[:company][:owner] && params[:company][:owner][:name]

This requires three checks, and makes for very ugly code. Any way to get around this?

16条回答
柔情千种
2楼-- · 2019-01-01 08:03

Ruby 2.3.0 introduced a new method called dig on both Hash and Array that solves this problem entirely.

name = params.dig(:company, :owner, :name)

It returns nil if the key is missing at any level.

If you are using a version of Ruby older than 2.3, you can use the ruby_dig gem or implement it yourself:

module RubyDig
  def dig(key, *rest)
    if value = (self[key] rescue nil)
      if rest.empty?
        value
      elsif value.respond_to?(:dig)
        value.dig(*rest)
      end
    end
  end
end

if RUBY_VERSION < '2.3'
  Array.send(:include, RubyDig)
  Hash.send(:include, RubyDig)
end
查看更多
其实,你不懂
3楼-- · 2019-01-01 08:06

The best compromise between functionality and clarity IMO is Raganwald's andand. With that, you would do:

params[:company].andand[:owner].andand[:name]

It's similar to try, but reads a lot better in this case since you're still sending messages like normal, but with a delimiter between that calls attention to the fact that you're treating nils specially.

查看更多
余生请多指教
4楼-- · 2019-01-01 08:07

Dangerous but works:

class Object
        def h_try(key)
            self[key] if self.respond_to?('[]')
        end    
end

We can new do

   user = { 
     :first_name => 'My First Name', 
     :last_name => 'my Last Name', 
     :details => { 
        :age => 3, 
        :birthday => 'June 1, 2017' 
      } 
   }

   user.h_try(:first_name) # 'My First Name'
   user.h_try(:something) # nil
   user.h_try(:details).h_try(:age) # 3
   user.h_try(:details).h_try(:nothing).h_try(:doesnt_exist) #nil

The "h_try" chain follows similar style to a "try" chain.

查看更多
查无此人
5楼-- · 2019-01-01 08:11

If you wanna get into monkeypatching you could do something like this

class NilClass
  def [](anything)
    nil
  end
end

Then a call to params[:company][:owner][:name] will yield nil if at any point one of the nested hashes is nil.

EDIT: If you want a safer route that also provides clean code you could do something like

class Hash
  def chain(*args)
    x = 0
    current = self[args[x]]
    while current && x < args.size - 1
      x += 1
      current = current[args[x]]
    end
    current
  end
end

The code would look like this: params.chain(:company, :owner, :name)

查看更多
零度萤火
6楼-- · 2019-01-01 08:12

I would write this as:

name = params[:company] && params[:company][:owner] && params[:company][:owner][:name]

It's not as clean as the ? operator in Io, but Ruby doesn't have that. The answer by @ThiagoSilveira is also good, though it will be slower.

查看更多
零度萤火
7楼-- · 2019-01-01 08:12
require 'xkeys' # on rubygems.org

params.extend XKeys::Hash # No problem that we got params from somebody else!
name = params[:company, :owner, :name] # or maybe...
name = params[:company, :owner, :name, :else => 'Unknown']
# Note: never any side effects for looking

# But you can assign too...
params[:company, :reviewed] = true
查看更多
登录 后发表回答