【问题标题】:Accessing elements of nested hashes in ruby [duplicate]访问ruby中嵌套哈希的元素[重复]
【发布时间】:2011-04-04 22:01:58
【问题描述】:

我正在开发一个用 ruby​​ 编写的小实用程序,它广泛使用嵌套哈希。目前,我正在检查对嵌套哈希元素的访问,如下所示:

structure = { :a => { :b => 'foo' }}

# I want structure[:a][:b]

value = nil

if structure.has_key?(:a) && structure[:a].has_key?(:b) then
  value = structure[:a][:b]
end

有没有更好的方法来做到这一点?我想说的是:

value = structure[:a][:b]

如果 :a 不是 structure 中的键,则获取 nil 等。

【问题讨论】:

  • Ruby 2.3 添加了Hash#dig 来解决这个问题。请参阅下面的答案。
  • 应该有一个 SO 徽章用于将五年前的问题标记为重复,另一个用于将五年前的问题标记为此类。成就解锁!
  • 如果您在 2.3 之前使用 Ruby, (structure[:a] || {})[:b] 应该可以解决问题

标签: ruby hash hash-of-hashes


【解决方案1】:

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

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

但是,Ruby 2.3 添加了一个方法Hash#dig,使这种方式更加优雅:

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

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

【讨论】:

  • 我认为你应该删除关于顶级哈希的位,因为这不会应用任意深度,所以h[:foo][:bar][:jim] 仍然会爆炸。
  • 使用默认哈希值的另一个问题是它不可持久化。如果您将数据结构转储到磁盘并稍后加载它,它将失去其默认状态,如果您通过网络发送它也是如此。与其他在发生这种情况时显式引发错误的类不同, Marshal.dump(Hash.new(foo)) 会成功地丢失您的默认值。
  • 你不是在介绍Hash.new({})的一点参考问题吗?不是每个默认条目最终都使用相同的哈希吗?
  • 是的,每个默认值都将使用相同的空哈希,但这没关系。 h[:new_key] = new_value 将创建一个新条目,并且不会修改默认值。
  • 但是如果你开始做类似h = Hash.new({}); h[:a][:b] = 1; h[:c][:d] = 2 这样的事情,你就会陷入混乱。
【解决方案2】:

HashArray 有一个 method called dig 可以完全解决这个问题。

value = structure.dig(:a, :b)

如果密钥在任何级别丢失,则返回nil

如果您使用的 Ruby 版本早于 2.3,则可以使用 ruby_dig gem。

【讨论】:

    【解决方案3】:

    这些天我通常这样做的方式是:

    h = Hash.new { |h,k| h[k] = {} }
    

    这将为您提供一个散列,该散列创建一个新散列作为缺少键的条目,但为第二级键返回 nil:

    h['foo'] -> {}
    h['foo']['bar'] -> nil
    

    您可以嵌套它以添加可以通过这种方式处理的多个层:

    h = Hash.new { |h, k| h[k] = Hash.new { |hh, kk| hh[kk] = {} } }
    
    h['bar'] -> {}
    h['tar']['zar'] -> {}
    h['scar']['far']['mar'] -> nil
    

    您也可以使用default_proc 方法无限链接:

    h = Hash.new { |h, k| h[k] = Hash.new(&h.default_proc) }
    
    h['bar'] -> {}
    h['tar']['star']['par'] -> {}
    

    上面的代码创建了一个散列,其默认过程创建了一个具有相同默认过程的新散列。因此,在查找未见过的键时创建为默认值的哈希将具有相同的默认行为。

    编辑:更多细节

    Ruby 哈希允许您控制在查找新键时如何创建默认值。指定后,此行为被封装为 Proc 对象,并可通过 default_procdefault_proc= 方法访问。也可以通过将块传递给Hash.new 来指定默认过程。

    让我们稍微分解一下这段代码。这不是惯用的 ruby​​,但它更容易分成多行:

    1. recursive_hash = Hash.new do |h, k|
    2.   h[k] = Hash.new(&h.default_proc)
    3. end
    

    第 1 行将变量recursive_hash 声明为新的Hash,并开始一个块为recursive_hashdefault_proc。该块传递了两个对象:h,这是正在执行键查找的Hash 实例,以及k,正在查找的键。

    第 2 行将散列中的默认值设置为新的 Hash 实例。这个散列的默认行为是通过传递一个Proc 来提供的,该Proc 是从发生查找的散列的default_proc 创建的;即,块本身定义的默认过程。

    这是一个 IRB 会话的示例:

    irb(main):011:0> recursive_hash = Hash.new do |h,k|
    irb(main):012:1* h[k] = Hash.new(&h.default_proc)
    irb(main):013:1> end
    => {}
    irb(main):014:0> recursive_hash[:foo]
    => {}
    irb(main):015:0> recursive_hash
    => {:foo=>{}}
    

    recursive_hash[:foo] 的哈希被创建时,它的default_procrecursive_hashdefault_proc 提供。这有两个效果:

    1. recursive_hash[:foo] 的默认行为与recursive_hash 相同。
    2. recursive_hash[:foo]default_proc 创建的哈希的默认行为将与recursive_hash 相同。

    所以,继续 IRB,我们得到以下结果:

    irb(main):016:0> recursive_hash[:foo][:bar]
    => {}
    irb(main):017:0> recursive_hash
    => {:foo=>{:bar=>{}}}
    irb(main):018:0> recursive_hash[:foo][:bar][:zap]
    => {}
    irb(main):019:0> recursive_hash
    => {:foo=>{:bar=>{:zap=>{}}}}
    

    【讨论】:

    • 嗨,保罗,你能帮我理解最后一个是如何工作的吗?我为此发布了一个单独的问题:stackoverflow.com/q/20158213/273333 谢谢!
    • Paul,请注意,您可以将 Hash.new {|h,k| h[k] = {}} 简化为 Hash.new({}) 和将 Hash.new {|h, k| h[k] = Hash.new { |hh, kk| hh[kk] = {}}} 简化为 Hash.new(Hash.new({}))
    • @mdm414ZX 我已经编辑了我的答案以提供更多详细信息。希望对您有所帮助。
    • 对于像我这样想要使用它但不知道如何为子哈希设置默认值的新手,请阅读this。不要在同一块石头上绊倒......
    • Ruby 2.3.0 添加了Hash#dig 来解决这个问题。看我的回答。
    【解决方案4】:

    我为此制作了 ruby​​gem。试试vine

    安装:

    gem install vine
    

    用法:

    hash.access("a.b.c")
    

    【讨论】:

    • 太棒了,我正在使用 Savon 调用 SharePoint Web 服务(这很棒),我的响应如下使用 Vine... ;-) data.vine("get_user_collection_from_group_response.get_user_collection_from_group_result.get_user_collection_from_group.users .user")
    • Vine 看起来很有趣,但您可能还想查看 Hashie,它是一个更完整的库。看我的回答。
    • 顺便说一句,这确实只适用于访问散列中的元素。不要期望能够使用此方法替换值。
    【解决方案5】:

    我认为最易读的解决方案之一是使用Hashie

    require 'hashie'
    myhash = Hashie::Mash.new({foo: {bar: "blah" }})
    
    myhash.foo.bar
    => "blah"    
    
    myhash.foo?
    => true
    
    # use "underscore dot" for multi-level testing
    myhash.foo_.bar?
    => true
    myhash.foo_.huh_.what?
    => false
    

    【讨论】:

      【解决方案6】:
      value = structure[:a][:b] rescue nil
      

      【讨论】:

      • 这会默默地将丢失的变量或方法等变为 nil。在某种程度上,这就是意图,但这是一把宽刀,也许应该是一个很好的切割。
      • 谢谢,@Wayne。很好的解释
      • 我对像这样使用rescue 非常谨慎,这与韦恩给出的相同原因,但也因为它可以掩盖你应该知道的逻辑或语法错误。查找以这种方式掩盖的错误可能很困难。
      【解决方案7】:

      解决方案 1

      我之前在我的问题中建议过这个:

      class NilClass; def to_hash; {} end end
      

      Hash#to_hash 已经定义,并返回 self。然后你可以这样做:

      value = structure[:a].to_hash[:b]
      

      to_hash 可确保在前一个键搜索失败时获得空哈希。

      解决方案2

      这个解决方案在精神上类似于 mu is too short 的答案,因为它使用了一个子类,但仍然有些不同。如果某个键没有值,它不会使用默认值,而是创建一个空哈希值,这样就不会像 DigitalRoss 的答案那样存在分配混乱的问题,正如指出的那样mu 太短了。

      class NilFreeHash < Hash
        def [] key; key?(key) ? super(key) : self[key] = NilFreeHash.new end
      end
      
      structure = NilFreeHash.new
      structure[:a][:b] = 3
      p strucrture[:a][:b] # => 3
      

      不过,它偏离了问题中给出的规范。当给定一个未定义的键时,它将返回一个空的散列插入nil

      p structure[:c] # => {}
      

      如果你从头构建这个 NilFreeHash 的一个实例并分配键值,它会起作用,但是如果你想将一个哈希转换成这个类的一个实例,那可能是个问题。

      【讨论】:

      • 为什么不将两个答案合二为一?
      • 他们是不相关的,我觉得还是分开比较好。您是建议将两种解决方案放在一篇文章中,还是将它们结合起来给出不同的答案?
      • 将它们放在一个答案中。这是 SO 上非常常见的做法,尤其是当您比较/对比它们时。
      • 酷。 觉得这样很好,因为更容易看出方法的差异。当它们是单独的答案时,这要困难得多。
      • 我明白了。我想我还在学习了解这里的习俗。
      【解决方案8】:

      您可以使用额外的可变参数方法构建一个 Hash 子类,以便一路向下挖掘,并在此过程中进行适当的检查。像这样的东西(当然有更好的名字):

      class Thing < Hash
        def find(*path)
          path.inject(self) { |h, x| return nil if(!h.is_a?(Thing) || h[x].nil?); h[x] }
        end
      end
      

      然后只需使用Things 代替哈希:

      >> x = Thing.new
      => {}
      >> x[:a] = Thing.new
      => {}
      >> x[:a][:b] = 'k'
      => "k"
      >> x.find(:a)
      => {:b=>"k"}
      >> x.find(:a, :b)
      => "k"
      >> x.find(:a, :b, :c)
      => nil
      >> x.find(:a, :c, :d)
      => nil
      

      【讨论】:

      • 为什么你没有从那个返回中得到一个本地跳转错误?您的代码绝对有效。我认为这大致等效: >> hash = {:a => {:b => 'k'}} >> [:a, :b].inject(hash) {|h, x|如果 (h[x].nil?|| !h[x].is_a?(Hash)); 返回 nil h[x] },但给出 LocalJumpError
      • @rainkinz:你得到一个 LocalJumpError 是因为你的块试图返回而不是在一个方法中(或者“一个声明块的方法”是迂腐的)。我的 return 有效,因为它是从 find 方法返回的,您的版本没有任何地方可以返回,所以 Ruby 非常适合。
      • 啊,当然。感谢您的解释。
      【解决方案9】:

      Hash 的这个猴子补丁函数应该是最简单的(至少对我来说)。它也不会改变结构,即将nil 更改为{}。即使您从原始来源读取树,它仍然适用,例如JSON。它也不需要在运行或解析字符串时产生空的哈希对象。 rescue nil 对我来说实际上是一个很好的简单解决方案,因为我有足够的勇气承担如此低的风险,但我发现它本质上在性能方面存在缺陷。

      class ::Hash
        def recurse(*keys)
          v = self[keys.shift]
          while keys.length > 0
            return nil if not v.is_a? Hash
            v = v[keys.shift]
          end
          v
        end
      end
      

      例子:

      > structure = { :a => { :b => 'foo' }}
      => {:a=>{:b=>"foo"}}
      
      > structure.recurse(:a, :b)
      => "foo"
      
      > structure.recurse(:a, :x)
      => nil
      

      还有一个好处是你可以用它来玩已保存的数组:

      > keys = [:a, :b]
      => [:a, :b]
      
      > structure.recurse(*keys)
      => "foo"
      
      > structure.recurse(*keys, :x1, :x2)
      => nil
      

      【讨论】:

        【解决方案10】:

        XKeys gem 将以简单、清晰、可读和紧凑的方式读取和自动激活写入嵌套哈希 (::Hash) 或哈希和数组 (::Auto,基于键/索引类型)通过增强#[] 和#[]= 来增强语法。标记符号 :[] 将推到数组的末尾。

        require 'xkeys'
        
        structure = {}.extend XKeys::Hash
        structure[:a, :b] # nil
        structure[:a, :b, :else => 0] # 0 (contextual default)
        structure[:a] # nil, even after above
        structure[:a, :b] = 'foo'
        structure[:a, :b] # foo
        

        【讨论】:

        【解决方案11】:

        您可以使用andand gem,但我越来越警惕它:

        >> structure = { :a => { :b => 'foo' }} #=> {:a=>{:b=>"foo"}}
        >> require 'andand' #=> true
        >> structure[:a].andand[:b] #=> "foo"
        >> structure[:c].andand[:b] #=> nil
        

        【讨论】:

        • 迈克尔,你的谨慎理由值得一提吗?
        • @Wayne:我发现在我们的应用程序中对andand 的虚假使用有时会涵盖一个很容易发现很长时间的问题。
        【解决方案12】:

        有一种可爱但错误的方法来做到这一点。即猴子补丁NilClass 添加一个返回nil[] 方法。我说这是错误的方法,因为你不知道其他软件可能制作了不同的版本,或者未来版本的 Ruby 中的哪些行为变化会因此而被破坏。

        更好的方法是创建一个与nil 很相似但支持这种行为的新对象。使这个新对象成为您的哈希值的默认返回。然后它就可以工作了。

        或者,您可以创建一个简单的“嵌套查找”函数,将散列和键传递给该函数,该函数按顺序遍历散列,并在可能的情况下中断。

        我个人更喜欢后两种方法之一。虽然我认为如果将第一个集成到 Ruby 语言中会很可爱。 (但猴子补丁是个坏主意。不要那样做。特别是不要证明你是一个多么酷的黑客。)

        【讨论】:

        • 哇,我以前从来没有被称为可爱。 :) 我同意这很方便,但最终使用起来太危险了。
        【解决方案13】:

        不是我会这样做,但你可以在 NilClass#[] 进行 Monkeypatch:

        > structure = { :a => { :b => 'foo' }}
        #=> {:a=>{:b=>"foo"}}
        
        > structure[:x][:y]
        NoMethodError: undefined method `[]' for nil:NilClass
                from (irb):2
                from C:/Ruby/bin/irb:12:in `<main>'
        
        > class NilClass; def [](*a); end; end
        #=> nil
        
        > structure[:x][:y]
        #=> nil
        
        > structure[:a][:y]
        #=> nil
        
        > structure[:a][:b]
        #=> "foo"
        

        选择@DigitalRoss 的回答。是的,它需要更多的打字,但那是因为它更安全。

        【讨论】:

          【解决方案14】:

          在我的例子中,我需要一个二维矩阵,其中每个单元格都是一个项目列表。

          我发现这种技术似乎有效。它可能适用于 OP:

          $all = Hash.new()
          
          def $all.[](k)
            v = fetch(k, nil)
            return v if v
          
            h = Hash.new()
            def h.[](k2)
              v = fetch(k2, nil)
              return v if v
              list = Array.new()
              store(k2, list)
              return list
            end
          
            store(k, h)
            return h
          end
          
          $all['g1-a']['g2-a'] << '1'
          $all['g1-a']['g2-a'] << '2'
          
          $all['g1-a']['g2-a'] << '3'
          $all['g1-a']['g2-b'] << '4'
          
          $all['g1-b']['g2-a'] << '5'
          $all['g1-b']['g2-c'] << '6'
          
          $all.keys.each do |group1|
            $all[group1].keys.each do |group2|
              $all[group1][group2].each do |item|
                puts "#{group1} #{group2} #{item}"
              end
            end
          end
          

          输出是:

          $ ruby -v && ruby t.rb
          ruby 1.9.2p0 (2010-08-18 revision 29036) [x86_64-linux]
          g1-a g2-a 1
          g1-a g2-a 2
          g1-a g2-a 3
          g1-a g2-b 4
          g1-b g2-a 5
          g1-b g2-c 6
          

          【讨论】:

            【解决方案15】:

            我目前正在尝试这个:

            # --------------------------------------------------------------------
            # System so that we chain methods together without worrying about nil
            # values (a la Objective-c).
            # Example:
            #   params[:foo].try?[:bar]
            #
            class Object
              # Returns self, unless NilClass (see below)
              def try?
                self
              end
            end  
            class NilClass
              class MethodMissingSink
                include Singleton
                def method_missing(meth, *args, &block)
                end
              end
              def try?
                MethodMissingSink.instance
              end
            end
            

            我知道反对 try 的论据,但它在调查事物时很有用,例如 params

            【讨论】:

              猜你喜欢
              • 2016-12-31
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2014-10-20
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多