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

时间:2009-11-30 15:22:35

标签: ruby hash coding-style

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

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

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

slate_has_children = !slate[:person][:children].nil?

那么,如果我们不知道“slate”是一个“人”哈希呢?考虑:

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

我们不能再轻易检查孩子:

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

那么,你如何检查哈希的结构,特别是如果它被深深嵌套(甚至比这里提供的例子更深)?也许更好的问题是:做这件事的“红宝石方式”是什么?

16 个答案:

答案 0 :(得分:78)

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

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

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

  

更新:如果您使用的是Ruby 2.3或更高版本,则会有一个内置的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 :(得分:20)

使用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)

答案 2 :(得分:13)

另一种选择:

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

答案 3 :(得分:4)

您可以使用andand gem:

require 'andand'

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

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

答案 4 :(得分: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

答案 5 :(得分:2)

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

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

然而,Ruby 2.3增加了一个使这种方式更优雅的功能:

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

有一个名为ruby_dig的宝石会为你补丁。

答案 6 :(得分:1)

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

请注意,在rails中你也可以这样做:

dino_has_children = !dino[person].try(:[], :children).nil?   # 

答案 7 :(得分:1)

这里有一种方法可以深入检查散列中的任何虚假值以及任何嵌套的散列,而无需猴子修补Ruby Hash类(请不要在Ruby类上使用猴子补丁,这是你不应该做的事情,EVER)。

(假设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

答案 8 :(得分:1)

在此简化上述答案:

创建一个Recursive Hash方法,其值不能为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]
=> {}

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

答案 9 :(得分:1)

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

这会将所有哈希变成一个然后变成任何一个?如果任何键与子字符串匹配,则返回true" children"存在。 这也可能有所帮助。

答案 10 :(得分:0)

您可以尝试使用

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 {}

答案 11 :(得分:0)

鉴于

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

您可以像这样检查 x y

(x[:a] || {})[:b] # 'c'
(y[:a] || {})[:b] # nil

答案 12 :(得分:0)

请问@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

答案 13 :(得分:0)

您还可以定义一个模块来对括号方法进行别名,并使用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"}}

答案 14 :(得分:0)

我不知道它是什么“ Ruby”(!),但是我编写的KeyDial gem基本上可以使您做到这一点,而无需更改原始语法:

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

成为:

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

这使用一些技巧来中介访问密钥。在call处,它将尝试在dig上的dino之前的键上输入,如果遇到错误(将这样),则返回nil。 nil?当然会返回true。

答案 15 :(得分:0)

您可以使用&key?的组合,将O(1)与dig的O(n)进行比较,这将确保没有{{ 1}}

NoMethodError: undefined method `[]' for nil:NilClass