如何避免嵌套散列中缺少元素的 NoMethodError,而无需重复的 nil 检查?

Posted

技术标签:

【中文标题】如何避免嵌套散列中缺少元素的 NoMethodError,而无需重复的 nil 检查?【英文标题】:How to avoid NoMethodError for missing elements in nested hashes, without repeated nil checks? 【发布时间】:2011-05-21 06:42:39 【问题描述】:

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

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

这需要三个检查,并且会产生非常丑陋的代码。有什么办法可以解决这个问题?

【问题讨论】:

在 groovy 中,您将使用 ? 运算符。实际上,我对等效的运算符感兴趣。您仍然可以扩展哈希类并添加运算符。 @Pasta Io 有类似的运算符,但 Ruby 没有。 【参考方案1】:

Ruby 2.3.0 在HashArray 上都引入了a new method called dig,完全解决了这个问题。

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

如果密钥在任何级别丢失,则返回nil

如果您使用的是 Ruby 2.3 之前的版本,您可以使用 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

【讨论】:

如果paramsnilparams.dig 将失败。考虑改用安全导航运算符或与.dig 结合使用:params&amp;.dig(:company, :owner, :name)params&amp;.company&amp;.owner&amp;.name 我之前的评论中安全导航运算符的哈希值语法不正确。正确的语法是:params&amp;.[](:company)&amp;.[](:owner)&amp;.[](:name)【参考方案2】:

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

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

它类似于try,但在这种情况下读起来要好得多,因为你仍然像往常一样发送消息,但在它之间有一个分隔符,提醒你注意你正在特别对待 nil 的事实。

【讨论】:

+1:我本来打算推荐可能的 Ick(也来自 Raganwald),这是同样的想法,你也可以在答案中包含一个链接:ick.rubyforge.org IMO andand 在语法上很恶心 @mpd:为什么?在概念上还是您只是不喜欢那个特定的词? @chuck 我喜欢这个概念,但它看起来很不优雅。如果您不知道它的用途,这也会令人困惑,我的意思是 andand 只是没有意义(我理解对 &amp;&amp; 的引用)。我不认为它用它的名字正确地传达了它的含义。话虽如此,我喜欢它胜过try【参考方案3】:

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

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

【讨论】:

很抱歉,但乱抢救是邪恶的,你可以掩盖这么多不相关的错误...... 是的,EEEEeeevil 带有大写字母“E”。 由于这里唯一发生的事情是带有符号的哈希查找,在我看来,这就像一个非常有区别的救援,而这正是我所做的。 您可以选择要捕获的异常,如下所示:***.com/questions/6224875/… @glennmcdonald 这段代码无法确保params 是一个哈希值。 rescue nil 仍然不行。这里发布了更好、更轻松的解决方案。没有理由冒险并尝试对此保持聪明。【参考方案4】:

您可能想研究一种将auto-vivification 添加到ruby 哈希的方法。以下***线程中提到了多种方法:

Ruby Autovivification ruby hash autovivification (facets)

【讨论】:

谢谢斯蒂芬。我以前从未听说过 auto-vivification,但如果我定义散列就完美了。感谢您的回答! 如何编辑您的答案并使链接更加明显。很难说最后两个指向什么。【参考方案5】:

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

【讨论】:

这里有个稍微改进的方法:***.com/questions/6224875/… 这里有一个比“稍微改进”的方法稍有改进的方法:***.com/a/27498050/199685【参考方案6】:

如果是轨道,使用

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

哦等等,那更难看。 ;-)

【讨论】:

我不会说它更丑。感谢凯尔的回复。【参考方案7】:

如果你想进入猴子补丁,你可以做这样的事情

class NilClass
  def [](anything)
    nil
  end
end

如果任何时候嵌套哈希值之一为 nil,则调用 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)

【讨论】:

我喜欢这个解决方案,因为它很聪明,而且代码非常干净。但是男孩,这对我来说确实很危险。您永远不会知道整个应用程序中的数组是否实际上是 nil。 是的,这是这种方法的一个很大的缺点。但是,可以在方法定义中执行一些其他技巧,以在发生这种情况时向您发出警告。这实际上只是一个想法,可以根据程序员的需求量身定制。 这行得通,但有点危险,因为你正在猴子修补 Ruby 的一个非常基本的部分,以完全不同的方式工作。 是的,我还是很怕猴子补丁!【参考方案8】:

我会这样写:

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

它不如? operator in Io 干净,但Ruby 没有。 @ThiagoSilveira 的回答也不错,虽然会慢一些。

【讨论】:

【参考方案9】:

你能避免使用多维散列,并使用

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

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

改为?

【讨论】:

感谢安德鲁的回复。我无法避免多维散列(不幸的是),因为散列是从外部库传递的。【参考方案10】:

把丑写一次,然后隐藏

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

【讨论】:

如果返回值是链中的最后一项,我认为这对于 OP(和常见)需求可能会更好,更有用。【参考方案11】:

(尽管这是一个非常古老的问题,但也许这个答案对于像我这样没有想到“开始救援”控制结构表达式的一些 *** 人来说很有用。)

我会使用 try catch 语句(开始用 ruby​​ 语言进行救援):

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

【讨论】:

如果我打错了 name = parms[:company][:owner][:name] 怎么办?代码很乐意与“John Doe”一起使用,而我可能永远不会注意到。 没错,在救援案例中应该为零,因为这就是问题所使用的。我现在看到蒂亚戈·西尔维拉的回答正是我的想法,但更优雅。【参考方案12】:

做:

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

【讨论】:

【参考方案13】:

您不需要访问原始哈希定义——您可以在使用 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),如果你做了任何“正常”的自动激活东西链接到上面你会得到一个空的未找到值的哈希值,它的计算结果是“真”。

你可以做这样的事情——它只检查定义的值并返回它们。您不能这样设置它们,因为我们无法知道调用是否在作业的 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 中所知道和喜爱的那样。

【讨论】:

【参考方案14】:

危险但有效:

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”链相似的风格。

【讨论】:

【参考方案15】:

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

从 Ruby 2.3.0 开始:

您也可以将&amp;. 称为“安全导航运算符”,如:params&amp;.[](:company)&amp;.[](:owner)&amp;.[](:name)。这个绝对安全。

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

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

所以以下任何一种都可以安全使用:

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

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

【讨论】:

【参考方案16】:

只是为了在dig 上提供一个单,试试我写的KeyDial gem。这本质上是dig 的包装器,但重要的区别是它永远不会出错。

如果链中的对象属于某种本身不能是diged 的类型,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? &amp;&amp;,还需要返回hash[:a][:d].is_a?(Hash) 检查。 KeyDial 可让您在没有此类检查或错误的情况下执行此操作:

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

【讨论】:

以上是关于如何避免嵌套散列中缺少元素的 NoMethodError,而无需重复的 nil 检查?的主要内容,如果未能解决你的问题,请参考以下文章

pr嵌套效果只有一部分

如何根据散列中的键/值查找键/值数据并将其添加到 Redis 中的散列?

如何交换散列中的键和值

Ruby如何格式化嵌套哈希

在局部敏感散列中搜索

设计一个重新散列函数......如何避免相同的散列?