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

47

我正在编写一个用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]

如果 structure 中没有键为 :a ,则会得到 nil 等返回值。


2
Ruby 2.3添加了Hash#dig来解决这个问题。请参见我下面的答案。 - user513951
1
应该为将五年前的问题标记为重复提供一个SO徽章,另外还应该为将五年前的问题标记为重复提供另一个徽章。 成就已解锁! - Paul Morie
如果您使用的是2.3之前的Ruby,则(structure[:a] || {})[:b]应该可以解决问题。 - Henok T
我不会使用Hash#dig,我会跟随@PaulMorie的答案,将使用安全导航运算符.& —— 就像在 h&.fetch(:a,nil).&fetch(:b,nil) 中,盲目地遍历嵌套哈希或混合结构,并处理不存在(nil)中间键。这种结构应该被尽量避免。dig.&方法都是在Ruby 2.3.0中引入的。 - Claudio Floreani
15个回答

66

传统上,您确实需要像这样做:

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

然而,Ruby 2.3添加了一个方法Hash#dig,使得这种方式更加优雅:
structure.dig :a, :b # nil if it misses anywhere along the way

有一个名为ruby_dig的宝石能够为您完成此项后补工作。


3
我认为你应该删除关于顶层哈希的部分,因为这并不适用于任意深度,所以 h[:foo][:bar][:jim] 仍然会出错。 - Phrogz
默认哈希值的另一个陷阱是它不可持久化。如果您将数据结构转储到磁盘并稍后加载它,它将失去其默认状态,如果您将其发送到网络上也是一样。与其他类不同,当发生这种情况时,Marshal.dump(Hash.new(foo))会愉快地成功,但会丢失您的默认值。 - John F. Miller
1
是的,每个默认值都将使用相同的空哈希表,但这没关系。h[:new_key] = new_value 将创建一个新条目,并不会修改默认值。 - DigitalRoss
4
但是如果你开始做像 h = Hash.new({}); h[:a][:b] = 1; h[:c][:d] = 2 这样的事情,你将陷入一片混乱。 - mu is too short
@Josh FYI,原因是这个答案在很晚的时候被编辑添加了正确的答案。 - user513951
显示剩余3条评论

48

HashArray都有一个叫做dig的方法。

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

如果在任何一级上缺少键,则返回nil


如果你使用的是比2.3版本更早的Ruby版本,你可以安装一个类似ruby_dig或者hash_dig_and_collect的gem包,或者自己实现这个功能:

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

32

我现在通常这样做:

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

第一行声明一个变量recursive_hash为新的Hash,并开始一个块作为recursive_hashdefault_proc。该块传递了两个对象:h,它是正在执行键查找的Hash实例,以及k,被查找的键。
第二行将哈希中的默认值设置为新的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_proc是由recursive_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=>{}}}}

1
嗨,保罗,你能帮我理解最后一个是如何工作的吗?我已经发布了一个关于此问题的单独问题:https://dev59.com/EnnZa4cB1Zd3GeqPqXq9 谢谢! - unpangloss
@mdm414ZX 我已经编辑了我的答案,提供更多细节。希望有所帮助。 - Paul Morie
对于像我这样的新手,想要使用它但不知道如何为子哈希设置默认值,请阅读此处。不要在同一块石头上绊倒... - JoseHdez_2
Ruby 2.3.0 添加了 Hash#dig 来解决这个问题。请查看我的答案。 - user513951
如果你想要一个计数器,你可以这样做:h = Hash.new { |h,k| h[k] = Hash.new(0) } - dawg
显示剩余3条评论

14
我为此制作了一个Ruby宝石包。请尝试使用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”) - Marc
1
Vine看起来很有趣,但你也可以看看Hashie,它是一个更完整的库。请参见我的答案。 - Javid Jamae
1
顺便提一下,这只适用于访问哈希表中的元素。不要指望能够使用此方法替换值。 - Tom

7

我认为最易读的解决方案之一是使用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

3
value = structure[:a][:b] rescue nil

2
这将会悄悄地把缺失的变量或方法等转换成nil。从某种意义上来说,这是有意为之的,但这是一种粗暴的方式,也许应该采用精细的方式进行处理。 - Wayne Conrad
我非常谨慎地使用rescue,原因与Wayne所说的相同,但也因为它可能掩盖了您应该知道的逻辑或语法错误。通过这种方式掩盖的错误很难发现。 - the Tin Man

2
你可以构建一个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

为什么你的返回语句没有产生本地跳转错误?你的代码肯定是有效的。我认为这段代码大致等同于:>> hash = {:a => {:b => 'k'}} >> [:a, :b].inject(hash) {|h, x| return nil if (h[x].nil? || !h[x].is_a?(Hash)); h[x] },但会产生LocalJumpError错误。 - rainkinz
1
@rainkinz:你遇到了LocalJumpError,因为你的块正在尝试在不在方法内部(或“声明了该块”的方法,以严谨一些)的情况下返回。我的return有效是因为它是从find方法返回的,而你的版本没有任何可以返回的地方,所以Ruby会抛出一个错误。 - mu is too short
啊,当然。谢谢你的解释。 - rainkinz

2

解决方案1

我在之前的问题中提出了这个建议:


class NilClass; def to_hash; {} end end

Hash#to_hash已经被定义,返回的是自身。因此你可以这样做:

value = structure[:a].to_hash[:b]
to_hash 确保在先前的键搜索失败时获得空哈希。 解决方案2 这个解决方案与mu is too short的答案类似,它使用了一个子类,但还是有所不同。如果某个键没有值,则不使用默认值,而是创建一个空哈希值,因此它不会像DigitalRoss的答案一样,在赋值时出现混淆问题,正如mu is too short所指出的那样。
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 的实例并分配了键值,它将能够运行。但是,如果你想要将一个哈希转换为这个类的实例,则可能会出现问题。

为什么不把你的两个答案合并成一个呢? - the Tin Man
它们是不相关的,我认为最好将它们分开。你是在建议将两个解决方案放在一个帖子中,还是将它们合并以给出不同的答案? - sawa
把它们放在一个答案中。这是SO上非常常见的做法,特别是当你进行比较/对比时。 - the Tin Man
很好。我认为这样很好,因为更容易看到不同方法之间的差异。如果它们是分开的答案,那就更难了。 - the Tin Man
我明白了。我想我还需要学习了解这里的习惯。 - sawa

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

1
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

对于那些感兴趣的人,这里是 "xkeys" 的源代码:https://gist.github.com/Dorian/d5330b42473f15c1019b26dd3519eaca - Dorian
https://rubygems.org/gems/xkeys/ - Brian K

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接