问题
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?
回答1:
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
回答2:
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.
回答3:
I don't know if that's what you want, but maybe you could do this?
name = params[:company][:owner][:name] rescue nil
回答4:
You may want to look into one of the ways to add auto-vivification to ruby hashes. There are a number of approaches mentioned in the following stackoverflow threads:
- Ruby Autovivification
- ruby hash autovivification (facets)
回答5:
Equivalent to the second solution that user mpd
suggested, only more idiomatic Ruby:
class Hash
def deep_fetch *path
path.inject(self){|acc, e| acc[e] if acc}
end
end
hash = {a: {b: {c: 3, d: 4}}}
p hash.deep_fetch :a, :b, :c
#=> 3
p hash.deep_fetch :a, :b
#=> {:c=>3, :d=>4}
p hash.deep_fetch :a, :b, :e
#=> nil
p hash.deep_fetch :a, :b, :e, :f
#=> nil
回答6:
If it's rails, use
params.try(:[], :company).try(:[], :owner).try(:[], :name)
Oh wait, that's even uglier. ;-)
回答7:
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)
回答8:
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.
回答9:
Are you able to avoid using a multi-dimensional hash, and use
params[[:company, :owner, :name]]
or
params[[:company, :owner, :name]] if params.has_key?([:company, :owner, :name])
instead?
回答10:
Write the ugliness once, then hide it
def check_all_present(hash, keys)
current_hash = hash
keys.each do |key|
return false unless current_hash[key]
current_hash = current_hash[key]
end
true
end
回答11:
(Even though it's a really old question maybe this answer will be useful for some stackoverflow people like me that did not think of the "begin rescue" control structure expression.)
I would do it with a try catch statement (begin rescue in ruby language):
begin
name = params[:company][:owner][:name]
rescue
#if it raises errors maybe:
name = 'John Doe'
end
回答12:
Do:
params.fetch('company', {}).fetch('owner', {})['name']
Also at each step, you can use an appropriate method built in NilClass
to escape from nil
, if it were array, string, or numeric. Just add to_hash
to the inventory of this list and use it.
class NilClass; def to_hash; {} end end
params['company'].to_hash['owner'].to_hash['name']
回答13:
You don't need access to the original hash definition -- you can override the [] method on the fly after you get it using h.instance_eval, e.g.
h = {1 => 'one'}
h.instance_eval %q{
alias :brackets :[]
def [] key
if self.has_key? key
return self.brackets(key)
else
h = Hash.new
h.default = {}
return h
end
end
}
But that's not going to help you with the code you have, because you're relying on an unfound value to return a false value (e.g., nil) and if you do any of the "normal" auto-vivification stuff linked to above you're going to end up with an empty hash for unfound values, which evaluates as "true".
You could do something like this -- it only checks for defined values and returns them. You can't set them this way, because we've got no way of knowing if the call is on the LHS of an assignment.
module AVHash
def deep(*args)
first = args.shift
if args.size == 0
return self[first]
else
if self.has_key? first and self[first].is_a? Hash
self[first].send(:extend, AVHash)
return self[first].deep(*args)
else
return nil
end
end
end
end
h = {1=>2, 3=>{4=>5, 6=>{7=>8}}}
h.send(:extend, AVHash)
h.deep(0) #=> nil
h.deep(1) #=> 2
h.deep(3) #=> {4=>5, 6=>{7=>8}}
h.deep(3,4) #=> 5
h.deep(3,10) #=> nil
h.deep(3,6,7) #=> 8
Again, though, you can only check values with it -- not assign them. So it's not real auto-vivification as we all know and love it in Perl.
回答14:
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
回答15:
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.
回答16:
TLDR; params&.dig(:company, :owner, :name)
As of Ruby 2.3.0:
You can also use &.
called the "safe navigation operator" as: params&.[](:company)&.[](:owner)&.[](:name)
. This one is perfectly safe.
Using dig
on params
is not actually safe as params.dig
will fail if params
is nil.
However you may combine the two as: params&.dig(:company, :owner, :name)
.
So either of the following is safe to use:
params&.[](:company)&.[](:owner)&.[](:name)
params&.dig(:company, :owner, :name)
回答17:
Just to offer a one-up on dig
, try the KeyDial gem which I wrote. This is essentially a wrapper for dig
but with the important difference that it will never hit you with an error.
dig
will still spit out an error if an object in the chain is of some type that can't itself be dig
ed.
hash = {a: {b: {c: true}, d: 5}}
hash.dig(:a, :d, :c) #=> TypeError: Integer does not have #dig method
In this situation dig
does not help you, and you need to go back not only to hash[:a][:d].nil? &&
but also hash[:a][:d].is_a?(Hash)
checks. KeyDial lets you do this without such checks or errors:
hash.call(:a, :d, :c) #=> nil
hash.call(:a, :b, :c) #=> true
来源:https://stackoverflow.com/questions/4371716/how-to-avoid-nomethoderror-for-missing-elements-in-nested-hashes-without-repeat