代码之家  ›  专栏  ›  技术社区  ›  Todd R

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

  •  53
  • Todd R  · 技术社区  · 15 年前

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

    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”是“person”的哈希值呢?考虑:

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

    我们再也不容易检查孩子了:

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

    那么,您将如何检查哈希的结构,特别是如果它嵌套得很深(甚至比这里提供的示例更深)?也许一个更好的问题是:做这个的“红宝石方法”是什么?

    15 回复  |  直到 6 年前
        1
  •  76
  •   tadman    8 年前

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

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

    使用nIL?只在使用false作为占位符值时才需要,实际上很少需要这样做。一般来说,您可以简单地测试它是否存在。

    更新 :如果您使用的是Ruby2.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?
    
        2
  •  20
  •   Mario Pérez Alarcón    9 年前

    使用Ruby2.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)
    
        3
  •  13
  •   Cameron Martin Jamie Weston    11 年前

    另一种选择:

    dino.fetch(:person, {})[:children]
    
        4
  •  4
  •   paradigmatic    15 年前

    你可以使用 andand 宝石:

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

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

        5
  •  2
  •   kirushik    15 年前

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

    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
    
        6
  •  2
  •   DigitalRoss    8 年前

    传统上,你必须这样做:

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

    然而,Ruby2.3添加了一个使这种方式更加优雅的特性:

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

    有一块宝石叫 ruby_dig 那会帮你补上这个。

        7
  •  1
  •   Marc-André Lafortune    15 年前
    dino_has_children = !dino.fetch(person, {})[:children].nil?
    

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

    dino_has_children = !dino[person].try(:[], :children).nil?   # 
    
        8
  •  1
  •   josiah    9 年前

    下面是一种方法,您可以在不修补Ruby哈希类的情况下,对哈希和任何嵌套哈希中的任何错误值进行深入检查(请不要修补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
    
        9
  •  1
  •   bharath    8 年前
    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。 这也可能有帮助。

        10
  •  1
  •   Abhi    6 年前

    简化上述答案:

    创建一个值不能为零的递归哈希方法,如下所示。

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

    如果您不介意为键()创建空哈希值,请执行以下操作:

        11
  •  0
  •   MBO    15 年前

    你可以试着玩

    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 {}
    
        12
  •  0
  •   wedesoft    11 年前

    鉴于

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

    你可以检查 X Y 这样地:

    (x[:a] || {})[:b] # 'c'
    (y[:a] || {})[:b] # nil
    
        13
  •  0
  •   gtournie    7 年前

    泰德曼回答。

    对于那些想要性能(并且坚持使用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
    
        14
  •  0
  •   Matias    7 年前

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

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

    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"}}
    
        15
  •  0
  •   Convincible    6 年前

    我不知道这是什么“红宝石”!)但是 KeyDial 我写的gem可以让您在不改变原始语法的情况下完成这项工作:

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

    变成:

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

    这使用了一些技巧来中介密钥访问调用。AT call ,它会尝试 dig 上一个键打开 dino ,如果它遇到错误(如它所愿),则返回nil。 nil? 然后当然返回真值。