43

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

并获取nilif :a 不是 的键structure,等等。

4

15 回答 15

60

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

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

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

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

有一个名为的 gemruby_dig可以为你回补这个。

于 2011-04-04T22:15:23.830 回答
43

HashArray有一个方法dig可以完全解决这个问题。

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

nil如果密钥在任何级别丢失,它就会返回。

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

于 2016-01-06T03:02:05.257 回答
31

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

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_hashnewHash并开始一个块为recursive_hash's default_proc。该块被传递了两个对象:h,这是Hash正在执行键查找的实例,以及k,正在查找的键。

第 2 行将散列中的默认值设置为新Hash实例。这个散列的默认行为是通过传递一个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=>{}}

创建哈希 atrecursive_hash[:foo]时,它default_procrecursive_hash's提供default_proc。这有两个效果:

  1. 的默认行为recursive_hash[:foo]与 相同recursive_hash
  2. recursive_hash[:foo]'s创建的哈希的默认行为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=>{}}}}
于 2013-06-28T02:58:48.367 回答
14

我为此制作了rubygem。试试

安装:

gem install vine

用法:

hash.access("a.b.c")
于 2011-12-08T14:15:59.577 回答
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
于 2013-12-18T01:07:31.103 回答
3
value = structure[:a][:b] rescue nil
于 2011-04-04T22:16:50.853 回答
2

You could just build a Hash subclass with an extra variadic method for digging all the way down with appropriate checks along the way. Something like this (with a better name of course):

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

Then just use Things instead of hashes:

>> 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
于 2011-04-04T23:16:55.987 回答
2

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

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

p structure[:c] # => {}

如果你从一开始就构建这个 NilFreeHash 的实例并分配键值,它会起作用,但如果你想将哈希转换为此类的实例,那可能会出现问题。

于 2011-04-05T03:40:36.753 回答
1

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
于 2014-07-05T17:39:30.337 回答
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
于 2014-04-08T22:37:03.770 回答
0

I am currently trying out this:

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

I know the arguments against try, but it is useful when looking into things, like say, params.

于 2013-03-28T15:37:40.357 回答
0

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

我发现这种技术似乎有效。它可能适用于 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
于 2012-12-28T21:11:57.023 回答
0

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

>> structure = { :a => { :b => 'foo' }} #=> {:a=>{:b=>"foo"}}
>> require 'andand' #=> true
>> structure[:a].andand[:b] #=> "foo"
>> structure[:c].andand[:b] #=> nil
于 2011-04-04T22:13:47.727 回答
0

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

更好的方法是创建一个工作起来很像nil但支持这种行为的新对象。使这个新对象成为您的哈希值的默认返回。然后它就会起作用。

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

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

于 2011-04-04T22:15:04.017 回答
0

不是我会这样做,但你可以在 Monkeypatch 中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 `<main>'

> class NilClass; def [](*a); end; end
#=> nil

> structure[:x][:y]
#=> nil

> structure[:a][:y]
#=> nil

> structure[:a][:b]
#=> "foo"

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

于 2011-04-04T22:16:27.843 回答