在ruby中访问嵌套哈希的元素

我正在使用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

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

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

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

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

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

Ruby 2.3.0在HashArray上引入了一个名为dig的新方法 ,完全解决了这个问题。

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

如果在任何级别缺少密钥,则返回nil

如果您使用的是早于2.3的Ruby版本,则可以使用ruby_dig gem或ruby_dig实现:

 module RubyDig def dig(key, *rest) if value = (self[key] rescue nil) if rest.empty? value elsif value.respond_to?(:dig) value.dig(*rest) end end end end if RUBY_VERSION < '2.3' Array.send(:include, RubyDig) Hash.send(:include, RubyDig) end 

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

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

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

编辑:更多细节

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

让我们把这段代码打破一点。 这不是惯用的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实例。 通过传递从发生查找的哈希的default_proc创建的Proc来提供此哈希的默认行为; 即,块本身正在定义的默认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[:foo]相同。
  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=>{}}}} 

我为此制作了rubygem。 试试藤蔓 。

安装:

 gem install vine 

用法:

 hash.access("abc") 

我认为最可读的解决方案之一是使用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 
 value = structure[:a][:b] rescue nil 

解决方案1

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

 class NilClass; def to_hash; {} end end 

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

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

to_hash确保在上一次键搜索失败时获得空哈希。

溶液2

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

 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的实例并分配键值,它将起作用,但如果要将哈希转换为此类的实例,则可能存在问题。

您可以使用额外的可变方法构建一个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 

然后只使用Thing而不是哈希:

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

哈希的这个猴子补丁函数应该是最简单的(至少对我而言)。 它也不会改变结构,即将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 

你可以使用andand gem,但我对它越来越警惕了:

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

有可爱但错误的方法来做到这一点。 哪个是修补NilClass来添加一个返回nil[]方法。 我说这是错误的方法,因为你不知道其他软件可能制作了不同的版本,或者未来版本的Ruby中的行为改变可以被这个打破。

更好的方法是创建一个像nil一样工作的新对象,但支持这种行为。 将此新对象设为哈希的默认返回值。 然后它就会起作用。

或者,您可以创建一个简单的“嵌套查找”函数,您可以将哈希和键传递给它,按顺序遍历哈希值,并在可能的情况下突破。

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

不是我会这样做,但你可以在NilClass#[]中使用NilClass#[]

 > 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 `
' > class NilClass; def [](*a); end; end #=> nil > structure[:x][:y] #=> nil > structure[:a][:y] #=> nil > structure[:a][:b] #=> "foo"

使用@ DigitalRoss的答案。 是的,它打字更多,但那是因为它更安全。

就我而言,我需要一个二维矩阵,其中每个单元格都是一个项目列表。

我发现这种技术似乎有效。 它可能适用于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 

我目前正在尝试这个:

 # -------------------------------------------------------------------- # 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这样的事情时它很有用。