如何在哈希中使用类似于.try()的方法来避免在空值时出现"undefined method"错误?

183

在Rails中,如果某个值不存在我们可以采取以下方法来避免出现错误:

@myvar = @comment.try(:body)

我在深入挖掘哈希表时,如何避免出现错误?

@myvar = session[:comments][@comment.id]["temp_value"] 
# [:comments] may or may not exist here
在上述情况下,session[:comments]try[@comment.id] 无法使用。有什么其他的方法可以实现相同的功能?

2
相关问题:https://dev59.com/WG855IYBdhLWcg3wYjSF - Andrew Grimm
5
Ruby 2.3 引入了 Hash#dig 方法,使得这里不再需要使用 try。现在 @baxang 给出的答案最好。 - user513951
Dig不会使try变得不必要,因为它仍然会在除哈希之外的其他对象上失败。例如nil。但是,使用dig与save运算符结合使用可以=> session&.dig(:comments, @comment.id, "temp_value") - Markus Andreas
12个回答

293
你忘记在 try 前面加上一个 .
@myvar = session[:comments].try(:[], @comment.id)

因为[]是在执行[@comment.id]时使用的方法名。


15
由于在 try 内使用 :[] 看起来有点奇怪,您也可以按如下方式编写:session[:comments].try(:fetch, @comment.id)。该代码与前一段相同,只是改用了 fetch 方法来获取 session[:comments] 中指定的键值对。 - svoop
22
如果未找到键,则fetch会引发错误,除非您传递了默认值。因此,您需要编写:session[:comments].try(:fetch, @comment.id, nil)。 - rigyt
7
自 Ruby 2.3 开始,try 方法不再必要。下面是 @baxang 给出的最佳答案。 - user513951
如果为空怎么办?我想返回 [],而不是 nil。 - kamal
1
抱歉,我对Ruby还不熟悉,这里的:[]是什么意思? - hongchangfirst

75

Ruby 2.3.0-preview1的发布公告中介绍了安全导航运算符。

安全导航运算符已经存在于C#,Groovy和Swift中,现在作为 obj&.foo 引入,以便更轻松地处理nil。同时还添加了Array#digHash#dig

这意味着从2.3开始以下代码:

account.try(:owner).try(:address)

可以改写为

account&.owner&.address

然而,需要注意的是&不能完全替代#try。看一下这个例子:

> 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

还包括类似的用法:Array#digHash#dig。现在这个功能已经被添加了。

city = params.fetch(:[], :country).try(:[], :state).try(:[], :city)

可以重写为

city = params.dig(:country, :state, :city)

再次强调,#dig方法不会复制#try方法的行为。因此,在返回值时要小心。例如,如果params[:country]返回一个整数,将引发TypeError: Integer does not have #dig method错误。


11
如果哈希值为nil,则会出现错误,这是针对那些有疑问的人。 - JustGage
2
请注意,这实际上无法与Rails会话一起使用,因为ActionDispatch :: Request :: Session没有实现#dig。 - epylinkn
2
如果 params[:country] 不是一个 hash 或者为 nil(例如一个 string),也会导致程序中断。 - Renan
6
.dig不同,&.不会在遇到nil时中断。因此,一个安全的实现方式是:params&.dig(:country, :state, :city) - thisismydesign

25

最美的解决方案是Mladen Jablanović的一篇旧答案,它允许您在散列中深入挖掘,而不像使用直接的.try()调用那样限制深度,如果你想保持代码的美观:

class Hash
  def get_deep(*fields)
    fields.inject(self) {|acc,e| acc[e] if acc}
  end
end

你应该小心处理各种对象(尤其是params),因为字符串和数组也响应于 :[] 方法,但返回的值可能与你想要的不同,并且当字符串或符号用作索引时,数组会引发异常。

这就是为什么在下面的建议格式方法中使用了.is_a?(Hash)(通常难看的) 测试而不是 (通常更好的) .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

如果没有使用这个丑陋的 "is_a?(Hash)" 作为保护,那么最后一个例子将会引发异常: "Symbol as array index (TypeError)"。


1
实际上,由于nil不是一个Hash,你可以简化为fields.inject(self) {|acc,e| acc[e] if acc.is_a?(Hash)}。但我有一种感觉,使用#respond_to可能会更好。 - riffraff
1
@riffraff:你说的acc & acc.is_a?()是完全正确的 - 可以认为这是一个错误;但是respond_to不起作用,因为String和许多其他对象也会响应:[],但是此方法的结果不是所需的。 - Arsen7
使用 object.try 的目的是因为 object 可能是 nil。而在你的情况下,nil.get_deep 将会引发异常。因此,你的解决方案并没有回答问题。 - Augustin Riedinger
问题说“当我深入挖掘哈希表时”,我并没有假设session可能是nil,但如果它确实是,那么调用session.try(:get_deep, :comments, @comment.id, "temp_value")也完全没问题。 - Arsen7

17

在哈希中正确使用try的方法是@session.try(:[], :comments)

@session.try(:[], :comments).try(:[], commend.id).try(:[], 'temp_value')

为什么不能嵌套?try适用于任何Object,而nil是一个Object,所以我认为以下代码可以工作:nil.try(:do).try(:do_not).try(:there_is_a_try) - Andrew Grimm
2
“不能嵌套”是错误的。但对于您特定的情况,我的赞赏是正确的。您需要做的是使用try with :[],如果要直接使用键,则需要使用fetch。 - Pablo Castellazzi

17

更新:从 Ruby 2.3 开始,请使用#dig

大多数响应 [] 的对象都希望传递一个整数参数,但 Hash 是一个例外,它将接受任何对象(比如字符串或符号)。

以下是 Arsen7 答案的稍微更健壮的版本,支持嵌套的数组、哈希,以及任何其他期望通过 [] 传递整数的对象。

这并不是完全可靠的,因为有人可能已经创建了一个实现 [] 但不接受整数参数的对象。然而,在常见的情况下(例如从 JSON 提取嵌套值(其中包含哈希和数组)),此解决方案非常有效:

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

它可以像Arsen7的解决方案一样使用,但也支持数组,例如:

json = { 'users' => [ { 'name' => { 'first_name' => 'Frank'} }, { 'name' => { 'first_name' => 'Bob' } } ] }

json.get_deep 'users', 1, 'name', 'first_name' # Pulls out 'Bob'

14

假设你要查找params[:user][:email],但不确定params中是否存在user。那么-

你可以尝试:

params[:user].try(:[], :email)
它将返回nil(如果user不存在或user中没有email)或者useremail的值。

14
@myvar = session.fetch(:comments, {}).fetch(@comment.id, {})["temp_value"]

从 Ruby 2.0 开始,您可以这样做:

@myvar = session[:comments].to_h[@comment.id].to_h["temp_value"]

从 Ruby 2.3 版本开始,你可以这样做:

@myvar = session.dig(:comments, @comment.id, "temp_value")

1
因为这不是 .try 的作用。 - Jeff Dickey
4
似乎Session没有实现dig方法: #ActionDispatch::Request::Session:0x007ffc6cafa698 的undefined method 'dig'。 - Petercopter

14

从 Ruby 2.3 开始,这变得更加容易。现在你可以使用 Hash#dig 方法(文档),而不是必须嵌套 try 语句或者定义自己的方法。

h = { foo: {bar: {baz: 1}}}

h.dig(:foo, :bar, :baz)           #=> 1
h.dig(:foo, :zot)                 #=> nil

或者在上面的例子中:

session.dig(:comments, @comment.id, "temp_value")

这样做的另一个好处是更像try,而不像上面的一些例子。如果任何参数导致哈希返回nil,则它将响应nil。


无法在会话哈希上使用dig。 - JBlake
1
最佳答案,与常规哈希一起正常工作。 - svelandiag

8

另一种方法:

@myvar = session[:comments][@comment.id]["temp_value"] rescue nil

这可能被认为有些危险,因为它可以隐藏太多东西,但我个人喜欢它。如果你想要更多的控制,可以考虑使用以下内容:
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

1

最近我再次尝试时,安德鲁的答案对我没有用。也许有些东西已经改变了?

@myvar = session[:comments].try('[]', @comment.id)

"

'[]'是用引号而不是符号:[]表示的。

"

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接