【问题标题】:Ruby Style: How to check whether a nested hash element existsRuby Style:如何检查嵌套的哈希元素是否存在
【发布时间】:2010-12-21 16:25:08
【问题描述】:

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

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

那么,您将如何检查哈希的结构,尤其是如果它嵌套很深(甚至比此处提供的示例更深)?也许更好的问题是:“Ruby 方式”是什么?

【问题讨论】:

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

标签: ruby hash coding-style


【解决方案1】:

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

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?

【讨论】:

  • 您将如何使用相同的方法在嵌套哈希中设置值?
  • 您必须编写一些东西来创建中间散列,而不是简单地测试它们是否存在。如果您只处理哈希,location[key] ||= { } 就足够了,但您必须提取最后一部分 final_key = path.pop 并在最后分配给它。
  • is_a?(hash) - 好主意。保持简洁明了:p
  • @Josiah 如果您在扩展核心类时遇到问题,最好完全避开 Rails,它在那里很流行。谨慎使用它可以使您的代码更清洁。积极使用会导致混乱。
  • @tadman fyi 通过这个 SO 答案,#dig 现在在 Ruby 主干中:ruby-lang.org/en/news/2015/11/11/ruby-2-3-0-preview1-released:D
【解决方案2】:

在 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)

【讨论】:

  • 如果您使用的是 Ruby rubygems.org/gems/ruby_dig
【解决方案3】:

另一种选择:

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

【讨论】:

    【解决方案4】:

    您可以使用andand gem:

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

    您可以在http://andand.rubyforge.org/找到更多解释。

    【讨论】:

      【解决方案5】:

      可以使用默认值为 {} 的散列 - 空散列。例如,

      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 嵌套在每个散列的深处。
      【解决方案6】:

      传统上,您确实必须这样做:

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

      但是,Ruby 2.3 添加了一个特性,使这种方式更加优雅:

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

      有一个名为 ruby_dig 的 gem 会为你回补这个。

      【讨论】:

        【解决方案7】:
        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?用于嵌套哈希并想出了这个。
        【解决方案8】:
        dino_has_children = !dino.fetch(person, {})[:children].nil?
        

        请注意,在 Rails 中您也可以这样做:

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

        【讨论】:

          【解决方案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
          

          【讨论】:

            【解决方案10】:

            在此处简化上述答案:

            创建一个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]
            => {}
            

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

            【讨论】:

              【解决方案11】:

              你可以试试玩

              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】:

                给定

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

                你可以像这样检查 xy

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

                【讨论】:

                  【解决方案13】:

                  感谢@tadman 的回答。

                  对于那些想要 perfs(并且被 ruby​​

                  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】:

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

                    【讨论】:

                      【解决方案15】:

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

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

                      变成:

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

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

                      【讨论】:

                        【解决方案16】:

                        您可以使用 &amp;key? 的组合,它是 O(1),而 dig 是 O(n),这将确保在没有 NoMethodError: undefined method `[]' for nil:NilClass 的情况下访问人员

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

                        【讨论】:

                          猜你喜欢
                          • 2020-02-14
                          • 2016-12-31
                          • 2015-04-25
                          • 2014-10-20
                          • 2015-10-05
                          • 1970-01-01
                          • 1970-01-01
                          • 1970-01-01
                          相关资源
                          最近更新 更多