Ruby风格:如何检查嵌套哈希元素是否存在

70

考虑一个存储在哈希表中的“人”对象。以下是两个例子:

fred = {:person => {:name => "Fred", :spouse => "Wilma", :children => {:child => {:name => "Pebbles"}}}}
slate = {:person => {:name => "Mr. Slate", :spouse => "Mrs. Slate"}} 
如果“人”没有子元素,“children”元素将不会出现。 因此,对于Slate先生,我们可以检查他是否有父母:
slate_has_children = !slate[:person][:children].nil?

那么,如果我们不知道“slate”是一个“person”哈希,会怎样呢?考虑以下情况:

dino = {:pet => {:name => "Dino"}}

我们现在无法轻易地检查子元素:

dino_has_children = !dino[:person][:children].nil?
NoMethodError: undefined method `[]' for nil:NilClass

那么,您如何检查哈希表的结构,特别是如果它嵌套得很深(甚至比这里提供的示例更深)?也许一个更好的问题是:用“Ruby方式”如何实现这个?


1
你为什么没有实现一个对象模型或者至少用一些验证方法装饰一些结构体呢?我认为你会因为试图在哈希表上添加语义而抓狂的。 - Chris McCauley
1
即使您拥有对象模型,有时也需要从哈希中提取数据以填充您的模型。例如,如果您从JSON流获取数据。 - paradigmatic
16个回答

101

最显而易见的方法是逐步检查:

has_children = slate[:person] && slate[:person][:children]

只有在使用false作为占位符值时才需要使用.nil?方法,但实际上这种情况很少见。通常你可以简单地测试它是否存在。

更新:如果你正在使用Ruby 2.3或更高版本,则内置了一个dig方法,可以执行本答案中所述的操作。

如果没有,你也可以定义自己的哈希“dig”方法,可以大大简化此过程:

class Hash
  def dig(*path)
    path.inject(self) do |location, key|
      location.respond_to?(:keys) ? location[key] : nil
    end
  end
end

这种方法会检查每一步并避免在调用nil时出错。对于浅层结构,该实用程序的效果有些受限,但对于深度嵌套的结构,我发现它是非常宝贵的:

has_children = slate.dig(:person, :children)

您也可以使其更加稳健,例如,测试是否实际填充了:children条目:

children = slate.dig(:person, :children)
has_children = children && !children.empty?

1
你会如何使用相同的方法来设置嵌套哈希中的值? - zeeraw
2
is_a?(hash) - 不错的想法。保持简短、简洁和简单 :p - zeeraw
1
@Josiah 如果你在扩展核心类方面遇到了问题,最好完全避开Rails,那里普遍存在这种情况。适度使用可以使您的代码更加清晰。过度使用会导致混乱。 - tadman
@tadman 这是我对 Ruby 的疑虑之一。我喜欢这门语言和社区的热情,但社区在这里那里进行猴子补丁的倾向会带来麻烦。尽管我不赞同,但我在 Rails 中忍受了它。 - josiah
3
@tadman,通过这个Stack Overflow回答,#dig现在已经在Ruby trunk中了:https://www.ruby-lang.org/en/news/2015/11/11/ruby-2-3-0-preview1-released/ :D - Gabe Kopley
显示剩余9条评论

25

6
如果您正在使用Ruby < 2.3,我刚刚发布了一个宝石(gem),添加了与2.3兼容的Hash#dig和Array#dig方法:https://rubygems.org/gems/ruby_dig - Colin Kelley

13

另一种选择:

dino.fetch(:person, {})[:children]

4
您可以使用andand宝石:
require 'andand'

fred[:person].andand[:children].nil? #=> false
dino[:person].andand[:children].nil? #=> true

您可以在http://andand.rubyforge.org/上找到更详细的解释。


2
简化以上答案如下:
创建一个递归哈希方法,其值不能为nil,如下所示。
def recursive_hash
  Hash.new {|key, value| key[value] = recursive_hash}
end

> slate = recursive_hash 
> slate[:person][:name] = "Mr. Slate"
> slate[:person][:spouse] = "Mrs. Slate"

> slate
=> {:person=>{:name=>"Mr. Slate", :spouse=>"Mrs. Slate"}}
slate[:person][:state][:city]
=> {}

如果您不介意在键值不存在时创建空哈希 :)

2

可以使用默认值为 {}(空哈希)的哈希表。例如:

dino = Hash.new({})
dino[:pet] = {:name => "Dino"}
dino_has_children = !dino[:person][:children].nil? #=> false

那也适用于已创建的哈希:
dino = {:pet=>{:name=>"Dino"}}
dino.default = {}
dino_has_children = !dino[:person][:children].nil? #=> false

或者您可以为nil类定义[]方法。
class NilClass
  def [](* args)
     nil
   end
end

nil[:a] #=> nil

你需要确保.default嵌套深入到每个哈希中。 - Sam Figueroa

2
def flatten_hash(hash)
  hash.each_with_object({}) do |(k, v), h|
    if v.is_a? Hash
      flatten_hash(v).map do |h_k, h_v|
        h["#{k}_#{h_k}"] = h_v
      end
    else
      h[k] = v
    end
  end
end

irb(main):012:0> fred = {:person => {:name => "Fred", :spouse => "Wilma", :children => {:child => {:name => "Pebbles"}}}}
=> {:person=>{:name=>"Fred", :spouse=>"Wilma", :children=>{:child=>{:name=>"Pebbles"}}}}

irb(main):013:0> slate = {:person => {:name => "Mr. Slate", :spouse => "Mrs. Slate"}}
=> {:person=>{:name=>"Mr. Slate", :spouse=>"Mrs. Slate"}}

irb(main):014:0> flatten_hash(fred).keys.any? { |k| k.include?("children") }
=> true

irb(main):015:0> flatten_hash(slate).keys.any? { |k| k.include?("children") }
=> false

这将把所有哈希平铺成一个,然后如果存在任何匹配子字符串“children”的键,则任何? 将返回true。这可能也有帮助。

我思考了一下@zeeraw的评论,使用is_a?来处理嵌套哈希,并得出了这个结果。 - bharath

2

传统上,你必须像这样做:

structure[:a] && structure[:a][:b]

然而,Ruby 2.3添加了一个功能,使得这种方式更加优雅:
structure.dig :a, :b # nil if it misses anywhere along the way

有一个叫做ruby_dig的宝石可以为您进行后向兼容。


1
dino_has_children = !dino.fetch(person, {})[:children].nil?

请注意,在Rails中您也可以这样做:
dino_has_children = !dino[person].try(:[], :children).nil?   # 

1
这里有一种方法,可以在不对 Ruby Hash 类进行猴子补丁的情况下,深度检查哈希中的任何 falsy 值和任何嵌套的哈希。(请不要在 Ruby 类上进行猴子补丁,这是绝对不应该做的事情,永远不要这样做。)(假设使用 Rails,尽管您可以轻松修改此方法以在 Rails 之外工作)。
def deep_all_present?(hash)
  fail ArgumentError, 'deep_all_present? only accepts Hashes' unless hash.is_a? Hash

  hash.each do |key, value|
    return false if key.blank? || value.blank?
    return deep_all_present?(value) if value.is_a? Hash
  end

  true
end

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