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

Ruby Style: How to check whether a nested hash element exists

提问人:Todd R 提问时间:11/30/2009 最后编辑:Gaurav SharmaTodd R 更新时间:5/23/2019 访问量:60877

问:

考虑存储在哈希中的“人”。两个例子是:

fred = {:person => {:name => "Fred", :spouse => "Wilma", :children => {:child => {:name => "Pebbles"}}}}
slate = {:person => {:name => "Mr. Slate", :spouse => "Mrs. Slate"}} 

如果“person”没有任何子元素,则“children”元素不存在。所以,对于斯莱特先生,我们可以检查他是否有父母:

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 方式”是什么?

Ruby 哈希 编码风格

评论

1赞 Chris McCauley 12/1/2009
您没有为此实现对象模型或至少使用验证方法装饰某些结构的任何原因。我认为你会让自己发疯,试图将语义添加到哈希中。
1赞 paradigmatic 12/1/2009
即使您有一个对象模型,有时也需要从哈希中提取数据来填充模型。例如,如果从 JSON 流获取数据。

答:

2赞 kirushik 11/30/2009 #1

可以使用默认值为 {} 的哈希 - 空哈希。例如

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

这也适用于已经创建的 Hash:

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

评论

0赞 Sam Figueroa 8/17/2012
您必须确保 .default 嵌套在每个哈希值中。
101赞 tadman 11/30/2009 #2

最明显的方法是简单地检查每个步骤:

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

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

更新:如果您使用的是 Ruby 2.3 或更高版本,则有一个内置的 dig 方法可以执行本答案中描述的操作。

如果没有,您还可以定义自己的 Hash “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 8/4/2011
您将如何使用相同的方法在嵌套哈希中设置值?
0赞 tadman 8/4/2011
你必须编写一些东西来创建中间哈希,而不是简单地测试它们是否存在。 如果您只处理哈希值就足够了,但您必须提取最后一部分并最终分配给它。location[key] ||= { }final_key = path.pop
2赞 zeeraw 8/10/2011
is_a?(hash) - 好主意。保持简短、简洁、:p
1赞 tadman 8/6/2015
@Josiah 如果你在扩展核心类时遇到问题,最好完全避开 Rails,它在那里很普遍。谨慎使用可以使您的代码更干净。积极使用会导致混乱。
3赞 Gabe Kopley 11/13/2015
@tadman通过这个 SO 答案,#dig 现在在 Ruby 主干中:ruby-lang.org/en/news/2015/11/11/ruby-2-3-0-preview1-released:D
0赞 MBO 11/30/2009 #3

你可以尝试玩

dino.default = {}

或者例如:

empty_hash = {}
empty_hash.default = empty_hash

dino.default = empty_hash

这样你就可以调用

empty_hash[:a][:b][:c][:d][:e] # and so on...
dino[:person][:children] # at worst it returns {}
1赞 Marc-André Lafortune 11/30/2009 #4
dino_has_children = !dino.fetch(person, {})[:children].nil?

请注意,在 rails 中,您还可以执行以下操作:

dino_has_children = !dino[person].try(:[], :children).nil?   # 
4赞 paradigmatic 12/1/2009 #5

您可以使用 Gem:andand

require 'andand'

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

您可以在 http://andand.rubyforge.org/ 找到进一步的解释。

0赞 wedesoft 7/15/2013 #6

鉴于

x = {:a => {:b => 'c'}}
y = {}

您可以像这样检查 XY

(x[:a] || {})[:b] # 'c'
(y[:a] || {})[:b] # nil
13赞 Cameron Martin 4/14/2014 #7

另一种选择:

dino.fetch(:person, {})[:children]
1赞 josiah 8/5/2015 #8

这里有一种方法,你可以对哈希中的任何虚假值和任何嵌套的哈希进行深入检查,而无需对 Ruby Hash 类进行猴子修补(请不要在 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
25赞 Mario Pérez Alarcón 11/13/2015 #9

在 Ruby 2.3 中,我们将支持安全导航运算符: https://www.ruby-lang.org/en/news/2015/11/11/ruby-2-3-0-preview1-released/

has_children现在可以写成:

has_children = slate[:person]&.[](:children)

dig也被添加:

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

评论

6赞 Colin Kelley 11/30/2015
如果您使用的是 Ruby < 2.3,我刚刚发布了一个 gem,它添加了与 2.3 兼容的 Hash#dig 和 Array#dig 方法: rubygems.org/gems/ruby_dig
2赞 Abhi 5/31/2016 #10

在这里简化上述答案:

创建一个值不能为 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赞 DigitalRoss 9/7/2016 #11

传统上,你真的必须做这样的事情:

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

然而,Ruby 2.3 增加了一个特性,使这种方式更加优雅:

structure.dig :a, :b # nil if it misses anywhere along the way

有一个叫做宝石的宝石会为你回修这个。ruby_dig

2赞 bharath 12/16/2016 #12
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。 这也可能有所帮助。

评论

0赞 bharath 12/16/2016
我对@zeeraw的评论进行了思考,以使用is_a?对于嵌套哈希并想出这个。
0赞 gtournie 7/11/2017 #13

Thks@tadman答案。

对于那些想要性能(并且坚持使用 ruby < 2.3)的人来说,这种方法快了 2.5 倍:

unless Hash.method_defined? :dig
  class Hash
    def dig(*path)
      val, index, len = self, 0, path.length
      index += 1 while(index < len && val = val[path[index]])
      val
    end
  end
end

如果你使用 RubyInline,这个方法会快 16 倍:

unless Hash.method_defined? :dig
  require 'inline'

  class Hash
    inline do |builder|
      builder.c_raw '
      VALUE dig(int argc, VALUE *argv, VALUE self) {
        rb_check_arity(argc, 1, UNLIMITED_ARGUMENTS);
        self = rb_hash_aref(self, *argv);
        if (NIL_P(self) || !--argc) return self;
        ++argv;
        return dig(argc, argv, self);
      }'
    end
  end
end
0赞 Matias 9/29/2017 #14

您还可以定义一个模块来别名括号方法,并使用 Ruby 语法来读/写嵌套元素。

更新:不要重写括号访问器,而是请求 Hash 实例来扩展模块。

module Nesty
  def []=(*keys,value)
    key = keys.pop
    if keys.empty? 
      super(key, value) 
    else
      if self[*keys].is_a? Hash
        self[*keys][key] = value
      else
        self[*keys] = { key => value}
      end
    end
  end

  def [](*keys)
    self.dig(*keys)
  end
end

class Hash
  def nesty
    self.extend Nesty
    self
  end
end

然后你可以做:

irb> a = {}.nesty
=> {}
irb> a[:a, :b, :c] = "value"
=> "value"
irb> a
=> {:a=>{:b=>{:c=>"value"}}}
irb> a[:a,:b,:c]
=> "value"
irb> a[:a,:b]
=> {:c=>"value"}
irb> a[:a,:d] = "another value"
=> "another value"
irb> a
=> {:a=>{:b=>{:c=>"value"}, :d=>"another value"}}
0赞 Convincible 1/9/2019 #15

我不知道它有多“Ruby”(!),但是我编写的KeyDial gem基本上可以让你在不改变原始语法的情况下做到这一点:

has_kids = !dino[:person][:children].nil?

成为:

has_kids = !dino.dial[:person][:children].call.nil?

这使用一些技巧来中介密钥访问调用。在 ,它将尝试 上一个键,如果它遇到错误(因为它会),则返回 nil。 然后当然返回 true。calldigdinonil?

0赞 aabiro 5/23/2019 #16

您可以使用 和 O(1) 的组合,而 O(n) 是 O(n),这将确保在没有&key?digNoMethodError: undefined method `[]' for nil:NilClass

fred[:person]&.key?(:children) //=>true
slate[:person]&.key?(:children)