如何避免NoMethodError在嵌套哈希中缺少元素,而不重复nil检查?

我正在寻找一种避免在深层嵌套哈希中检查每个级别的nil的好方法。 例如:

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

这需要三次检查,并且会产生非常难看的代码。 有办法解决这个问题吗?

Ruby 2.3.0在HashArray上引入了一个名为dig的新方法 ,完全解决了这个问题。

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

如果在任何级别缺少密钥,则返回nil

如果您使用的是早于2.3的Ruby版本,则可以使用ruby_dig gem或自行实现:

 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 

function性和清晰度之间的最佳折衷IMO是Raganwald和。 有了它,你会做:

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

它类似于try ,但在这种情况下读取效果要好得多,因为你仍然像正常一样发送消息,但是在它之间有一个分隔符,要求注意你要特别处理nils的事实。

我不知道这是不是你想要的,但也许你可以这样做?

 name = params[:company][:owner][:name] rescue nil 

相当于用户mpd建议的第二个解决方案,只有更惯用的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 

如果是轨道,请使用

 params.try(:[], :company).try(:[], :owner).try(:[], :name) 

哦,等等,这甚至更加丑陋。 😉

如果你想进入monkeypatching,你可以做这样的事情

 NilClass类
   def [](任何东西)
    零
  结束
结束

然后调用params[:company][:owner][:name]将产生nil,如果在任何时候嵌套哈希值之一为nil。

编辑:如果你想要一个更安全的路线,也提供干净的代码,你可以做类似的事情

哈希
   def chain(* args)
     x = 0
     current = self [args [x]]
    而当前&& x 

代码如下所示: params.chain(:company, :owner, :name)

您可能想要研究一种向ruby哈希添加自动生成的方法。 以下stackoverflow线程中提到了许多方法:

  • Ruby Autovivification
  • ruby hash autovivification(facets)

我会这样写:

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

它没有那么干净? Io中的运算符 ,但Ruby没有。 @ThiagoSilveira的答案也很好,但速度会慢一些。

您是否能够避免使用多维哈希,并使用

 params[[:company, :owner, :name]] 

要么

 params[[:company, :owner, :name]] if params.has_key?([:company, :owner, :name]) 

代替?

写一次丑陋,然后隐藏它

 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 

(即使这是一个非常古老的问题,也许这个答案对于像我这样没有想到“开始救援”控制结构表达的一些stackoverflow人会有用。)

我会尝试使用try catch语句(以ruby语言开始救援):

 begin name = params[:company][:owner][:name] rescue #if it raises errors maybe: name = 'John Doe' end 

做:

 params.fetch('company', {}).fetch('owner', {})['name'] 

此外,在每个步骤中,您可以使用NilClass构建的适当方法从nil转义(如果它是数组,字符串或数字)。 只需将to_hash添加到此列表的清单中即可使用它。

 class NilClass; def to_hash; {} end end params['company'].to_hash['owner'].to_hash['name'] 

您不需要访问原始哈希定义 – 您可以在使用h.instance_eval获取后立即覆盖[]方法,例如

 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 } 

但这不会帮助你使用你拥有的代码,因为你依靠一个不合理的值来返回一个假值(例如,nil),如果你做任何与上面链接的“正常”自动生成的东西你’最终将得到一个空的哈希值,用于表示未实现的值,其值为“true”。

你可以这样做 – 它只检查定义的值并返回它们。 你不能这样设置它们,因为我们无法知道调用是否在作业的LHS上。

 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 

但是,您只能使用它来检查值 – 而不是分配它们。 所以它并不是真正的自动生存,因为我们都知道并喜欢Perl。

 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 

危险但有效:

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

我们可以新做

  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 

“h_try”链遵循与“try”链类似的风格。

从Ruby 2.3.0开始:

你也可以使用&. 称为“安全导航操作员”为: params&.[](:company)&.[](:owner)&.[](:name) 。 这个非常安全。

使用dig 并不安全,因为如果params为nil, params.dig(:company, :owner, :name)将失败。

但是,您可以将两者合并为: params&.dig(:company, :owner, :name)

因此,以下任何一种都可以安全使用:

params&.[](:company)&.[](:owner)&.[](:name)

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