可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
This question already has an answer here:
-
How to avoid NoMethodError for nil elements when accessing nested hashes? [duplicate]
4 answers
In Rails we can do the following in case a value doesn't exist to avoid an error:
@myvar = @comment.try(:body)
What is the equivalent when I'm digging deep into a hash and don't want to get an error?
@myvar = session[:comments][@comment.id]["temp_value"]
# [:comments] may or may not exist here
In the above case, session[:comments]try[@comment.id]
doesn't work. What would?
回答1:
You forgot to put a .
before the try
:
@myvar = session[:comments].try(:[], @comment.id)
since []
is the name of the method when you do [@comment.id]
.
回答2:
The announcement of Ruby 2.3.0-preview1 includes an introduction of Safe navigation operator.
A safe navigation operator, which already exists in C#, Groovy, and
Swift, is introduced to ease nil handling as obj&.foo
. Array#dig
and
Hash#dig
are also added.
This means as of 2.3 below code
account.try(:owner).try(:address)
can be rewritten to
account&.owner&.address
However, one should be careful that &
is not a drop in replacement of #try
. Take a look at this example:
> params = nil
nil
> params&.country
nil
> params = OpenStruct.new(country: "Australia")
#<OpenStruct country="Australia">
> params&.country
"Australia"
> params&.country&.name
NoMethodError: undefined method `name' for "Australia":String
from (pry):38:in `<main>'
> params.try(:country).try(:name)
nil
It is also including a similar sort of way: Array#dig
and Hash#dig
. So now this
city = params.fetch(:[], :country).try(:[], :state).try(:[], :city)
can be rewritten to
city = params.dig(:country, :state, :city)
Again, #dig
is not replicating #try
's behaviour. So be careful with returning values. If params[:country]
returns, for example, an Integer, TypeError: Integer does not have #dig method
will be raised.
回答3:
The most beautiful solution is an old answer by Mladen Jablanović, as it lets you to dig in the hash deeper than you could with using direct .try()
calls, if you want the code still look nice:
class Hash
def get_deep(*fields)
fields.inject(self) {|acc,e| acc[e] if acc}
end
end
You should be careful with various objects (especially params
), because Strings and Arrays also respond to :[], but the returned value may not be what you want, and Array raises exception for Strings or Symbols used as indexes.
That is the reason why in the suggested form of this method (below) the (usually ugly) test for .is_a?(Hash)
is used instead of (usually better) .respond_to?(:[])
:
class Hash
def get_deep(*fields)
fields.inject(self) {|acc,e| acc[e] if acc.is_a?(Hash)}
end
end
a_hash = {:one => {:two => {:three => "asd"}, :arr => [1,2,3]}}
puts a_hash.get_deep(:one, :two ).inspect # => {:three=>"asd"}
puts a_hash.get_deep(:one, :two, :three ).inspect # => "asd"
puts a_hash.get_deep(:one, :two, :three, :four).inspect # => nil
puts a_hash.get_deep(:one, :arr ).inspect # => [1,2,3]
puts a_hash.get_deep(:one, :arr, :too_deep ).inspect # => nil
The last example would raise an exception: "Symbol as array index (TypeError)" if it was not guarded by this ugly "is_a?(Hash)".
回答4:
The proper use of try with a hash is @sesion.try(:[], :comments)
.
@session.try(:[], :comments).try(:[], commend.id).try(:[], 'temp_value')
回答5:
Update: As of Ruby 2.3 use #dig
Most objects that respond to [] expect an Integer argument, with Hash being an exception that will accept any object (such as strings or symbols).
The following is a slightly more robust version of Arsen7's answer that supports nested Array, Hash, as well as any other objects that expect an Integer passed to [].
It's not fool proof, as someone may have created an object that implements [] and does not accept an Integer argument. However, this solution works great in the common case e.g. pulling nested values from JSON (which has both Hash and Array):
class Hash
def get_deep(*fields)
fields.inject(self) { |acc, e| acc[e] if acc.is_a?(Hash) || (e.is_a?(Integer) && acc.respond_to?(:[])) }
end
end
It can be used the same as Arsen7's solution but also supports arrays e.g.
json = { 'users' => [ { 'name' => { 'first_name' => 'Frank'} }, { 'name' => { 'first_name' => 'Bob' } } ] }
json.get_deep 'users', 1, 'name', 'first_name' # Pulls out 'Bob'
回答6:
@myvar = session.fetch(:comments, {}).fetch(@comment.id, {})["temp_value"]
From Ruby 2.0, you can do:
@myvar = session[:comments].to_h[@comment.id].to_h["temp_value"]
From Ruby 2.3, you can do:
@myvar = session.dig(:comments, @comment.id, "temp_value")
回答7:
say you want to find params[:user][:email]
but it's not sure whether user
is there in params
or not. Then-
you can try:
params[:user].try(:[], :email)
It will return either nil
(if user
is not there or email
is not there in user
) or otherwise the value of email
in user
.
回答8:
As of Ruby 2.3 this gets a little easier. Instead of having to nest try
statements or define your own method you can now use Hash#dig
(documentation).
h = { foo: {bar: {baz: 1}}}
h.dig(:foo, :bar, :baz) #=> 1
h.dig(:foo, :zot) #=> nil
Or in the example above:
session.dig(:comments, @comment.id, "temp_value")
This has the added benefit of being more like try
than some of the examples above. If any of the arguments lead to the hash returning nil then it will respond nil.
回答9:
Another approach:
@myvar = session[:comments][@comment.id]["temp_value"] rescue nil
This might also be consider a bit dangerous because it can hide too much, personally I like it.
If you want more control, you may consider something like:
def handle # just an example name, use what speaks to you
raise $! unless $!.kind_of? NoMethodError # Do whatever checks or
# reporting you want
end
# then you may use
@myvar = session[:comments][@comment.id]["temp_value"] rescue handle
回答10:
When you do this:
myhash[:one][:two][:three]
You're just chaining a bunch of calls to a "[]" method, an the error occurs if myhash[:one] returns nil, because nil doesn't have a [] method. So, one simple and rather hacky way is to add a [] method to Niclass, which returns nil: i would set this up in a rails app as follows:
Add the method:
#in lib/ruby_extensions.rb
class NilClass
def [](*args)
nil
end
end
Require the file:
#in config/initializers/app_environment.rb
require 'ruby_extensions'
Now you can call nested hashes without fear: i'm demonstrating in the console here:
>> hash = {:foo => "bar"}
=> {:foo=>"bar"}
>> hash[:foo]
=> "bar"
>> hash[:doo]
=> nil
>> hash[:doo][:too]
=> nil
回答11:
Andrew's answer didn't work for me when I tried this again recently. Maybe something has changed?
@myvar = session[:comments].try('[]', @comment.id)
The '[]'
is in quotes instead of a symbol :[]
回答12:
Try to use
@myvar = session[:comments][@comment.id]["temp_value"] if session[:comments]