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

时间:2010-12-06 22:47:36

标签: ruby-on-rails ruby hash hash-of-hashes

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

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

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

17 个答案:

答案 0 :(得分:28)

Ruby 2.3.0在HashArray上引入了a new method called 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

答案 1 :(得分:11)

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

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

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

答案 2 :(得分:6)

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

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

答案 3 :(得分:4)

您可能想要查看将auto-vivification添加到ruby哈希的方法之一。以下stackoverflow线程中提到了许多方法:

答案 4 :(得分:4)

相当于用户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

答案 5 :(得分:3)

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

class NilClass
  def [](anything)
    nil
  end
end

如果在任何一个嵌套哈希值为零的情况下,对params[:company][:owner][:name]的调用将产生nil。

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

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

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

答案 6 :(得分:3)

如果是轨道,请使用

params.try(:[], :company).try(:[], :owner).try(:[], :name)
哦,等等,这甚至更加丑陋。 ; - )

答案 7 :(得分:2)

我会把它写成:

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

它不像? operator in Io那么干净,但Ruby没有那个。 @ThiagoSilveira的答案也很好,但速度会慢一些。

答案 8 :(得分:1)

您是否可以避免使用多维哈希,并使用

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

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

代替?

答案 9 :(得分:1)

写一次丑陋,然后隐藏它

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

答案 10 :(得分:1)

执行:

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']

答案 11 :(得分:1)

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

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

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

答案 12 :(得分:0)

仅需提供dig的单张图片,请尝试我编写的KeyDial宝石。本质上,这是dig的包装器,但重要的区别在于它永远不会出错。

如果链中的某个对象本身无法dig编辑,则

dig仍然会抛出错误。

hash = {a: {b: {c: true}, d: 5}}

hash.dig(:a, :d, :c) #=> TypeError: Integer does not have #dig method

在这种情况下,dig对您没有帮助,您不仅需要返回hash[:a][:d].nil? &&,而且还要返回hash[:a][:d].is_a?(Hash)检查。 KeyDial可以让您执行此操作,而不会出现此类检查或错误:

hash.call(:a, :d, :c) #=> nil
hash.call(:a, :b, :c) #=> true

答案 13 :(得分:0)

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

从Ruby 2.3.0开始:

您还可以将名为“安全导航操作员”的&.用作:params&.[](:company)&.[](:owner)&.[](:name)。这个是非常安全的。

dig上使用params实际上并不安全,因为如果params.dig为零,params将会失败。

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

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

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

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

答案 14 :(得分:0)

危险但有效:

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

&#34; h_try&#34;链条遵循类似于&#34;尝试&#34;链

答案 15 :(得分:0)

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

答案 16 :(得分:0)

您不需要访问原始哈希定义 - 您可以在使用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。