如何避免在嵌套哈希中缺少元素时出现NoMethodError,而无需重复使用nil检查?

32

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

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

这需要进行三个检查,会导致代码非常丑陋。有没有什么方法可以避免这种情况?


1
在Groovy中,您将使用?运算符。实际上,我对等效运算符很感兴趣。您仍然可以扩展哈希类并添加该运算符。 - Pasta
@Pasta Io 有一个类似的运算符,但 Ruby 没有。 - Phrogz
16个回答

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

我认为如果返回值是链中的最后一个项目,这可能会更好、更有用于 OP(以及常见)的需求。 - Phrogz

1

虽然这是一个非常老的问题,但这个答案可能对一些像我这样没有考虑过“begin rescue”控制结构表达式的stackoverflow用户有用。

我会使用try catch语句(在ruby语言中为begin rescue)来实现:

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

如果我打错了 name = parms[:company][:owner][:name] 怎么办?代码会开心地继续执行 'John Doe',而我可能永远不会注意到。 - Edward Anderson
这是真的,在 rescue 情况下应该是 nil,因为这是问题所使用的。我现在看到 Thiago Silveira 的答案正是我所想的,但更加优雅。 - Trond Hatlen

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

"h_try"链采用与"try"链类似的风格。


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”。

你可以像这样做——它只检查定义了的值并返回它们。你不能通过这种方式设置它们,因为我们无法知道调用是否在赋值的左侧。

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 中所熟知和喜爱的真正的自动化创建。


0

简而言之:params&.dig(:company, :owner, :name)

从Ruby 2.3.0开始:

你也可以使用称为“安全导航运算符”的 &. 来实现:params&.[](:company)&.[](:owner)&.[](:name)。这个是完全安全的。

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

但是你可以将两者结合起来使用:params&.dig(:company, :owner, :name)

因此,以下任何一种方法都是安全的:

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

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


0

为了超越dig,可以尝试我编写的KeyDial gem。这本质上是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

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