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

发布于 2024-10-30 19:53:48 字数 419 浏览 1 评论 0原文

我正在编写一个用 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 不是 struct 等中的键,则得到 nil

I'm working a little utility written in ruby that makes extensive use of nested hashes. Currently, I'm checking access to nested hash elements as follows:

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

Is there a better way to do this? I'd like to be able to say:

value = structure[:a][:b]

And get nil if :a is not a key in structure, etc.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(15

梦里梦着梦中梦 2024-11-06 19:53:48

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

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

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

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

有一个名为 ruby_dig 将为您进行反向修补。

Traditionally, you really had to do something like this:

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

However, Ruby 2.3 added a method Hash#dig that makes this way more graceful:

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

There is a gem called ruby_dig that will back-patch this for you.

寒冷纷飞旳雪 2024-11-06 19:53:48

HashArray 有一个 名为 dig 的方法。

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

如果任何级别的密钥丢失,它都会返回nil


如果您使用的 Ruby 版本早于 2.3,您可以安装 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

Hash and Array have a method called dig.

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

It returns nil if the key is missing at any level.


If you are using a version of Ruby older than 2.3, you can install a gem such as ruby_dig or hash_dig_and_collect, or implement this functionality yourself:

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
幸福%小乖 2024-11-06 19:53:48

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

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 实例。该哈希的默认行为是通过传递一个从发生查找的哈希的 default_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_hash 的 <代码>default_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=>{}}}}

The way I usually do this these days is:

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

This will give you a hash that creates a new hash as the entry for a missing key, but returns nil for the second level of key:

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

You can nest this to add multiple layers that can be addressed this way:

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

h['bar'] -> {}
h['tar']['zar'] -> {}
h['scar']['far']['mar'] -> nil

You can also chain indefinitely by using the default_proc method:

h = Hash.new { |h, k| h[k] = Hash.new(&h.default_proc) }

h['bar'] -> {}
h['tar']['star']['par'] -> {}

The above code creates a hash whose default proc creates a new Hash with the same default proc. So, a hash created as a default value when a lookup for an unseen key occurs will have the same default behavior.

EDIT: More details

Ruby hashes allow you to control how default values are created when a lookup occurs for a new key. When specified, this behavior is encapsulated as a Proc object and is reachable via the default_proc and default_proc= methods. The default proc can also be specified by passing a block to Hash.new.

Let's break this code down a little. This is not idiomatic ruby, but it's easier to break it out into multiple lines:

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

Line 1 declares a variable recursive_hash to be a new Hash and begins a block to be recursive_hash's default_proc. The block is passed two objects: h, which is the Hash instance the key lookup is being performed on, and k, the key being looked up.

Line 2 sets the default value in the hash to a new Hash instance. The default behavior for this hash is supplied by passing a Proc created from the default_proc of the hash the lookup is occurring in; ie, the default proc the block itself is defining.

Here's an example from an IRB session:

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=>{}}

When the hash at recursive_hash[:foo] was created, its default_proc was supplied by recursive_hash's default_proc. This has two effects:

  1. The default behavior for recursive_hash[:foo] is the same as recursive_hash.
  2. The default behavior for hashes created by recursive_hash[:foo]'s default_proc will be the same as recursive_hash.

So, continuing in IRB, we get the following:

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=>{}}}}
南风几经秋 2024-11-06 19:53:48

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

安装:

gem install vine

使用:

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

I made rubygem for this. Try vine.

Install:

gem install vine

Usage:

hash.access("a.b.c")
会发光的星星闪亮亮i 2024-11-06 19:53:48

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

I think one of the most readable solutions is using 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
看海 2024-11-06 19:53:48
value = structure[:a][:b] rescue nil
value = structure[:a][:b] rescue nil
想你的星星会说话 2024-11-06 19:53:48

解决方案 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 的实例并分配键值,它会起作用,但如果您想将哈希转换为此类的实例,这可能是一个问题。

Solution 1

I suggested this in my question before:

class NilClass; def to_hash; {} end end

Hash#to_hash is already defined, and returns self. Then you can do:

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

The to_hash ensures that you get an empty hash when the previous key search fails.

Solution2

This solution is similar in spirit to mu is too short's answer in that it uses a subclass, but still somewhat different. In case there is no value for a certain key, it does not use a default value, but rather creates a value of empty hash, so that it does not have the problem of confusion in assigment that DigitalRoss's answer has, as was pointed out by 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

It departs from the specification given in the question, though. When an undefined key is given, it will return an empty hash instread of nil.

p structure[:c] # => {}

If you build an instance of this NilFreeHash from the beginning and assign the key-values, it will work, but if you want to convert a hash into an instance of this class, that may be a problem.

赴月观长安 2024-11-06 19:53:48

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

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
属性 2024-11-06 19:53:48

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

This monkey patch function for Hash should be easiest (at least for me). It also doesn't alter structure i.e. changing nil's to {}. It would still also apply even if you're reading a tree from a raw source e.g. JSON. It also doesn't need to produce empty hash objects as it goes or parse a string. rescue nil was actually a good easy solution for me as I'm brave enough for such a low risk but I find it to essentially have a drawback with performance.

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

Example:

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

> structure.recurse(:a, :b)
=> "foo"

> structure.recurse(:a, :x)
=> nil

What's also good is that you can play around saved arrays with it:

> keys = [:a, :b]
=> [:a, :b]

> structure.recurse(*keys)
=> "foo"

> structure.recurse(*keys, :x1, :x2)
=> nil
天荒地未老 2024-11-06 19:53:48

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

The XKeys gem will read and auto-vivify-on-write nested hashes (::Hash) or hashes and arrays (::Auto, based on the key/index type) with a simple, clear, readable, and compact syntax by enhancing #[] and #[]=. The sentinel symbol :[] will push onto the end of an array.

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
一梦浮鱼 2024-11-06 19:53:48

您可以使用 andand gem,但我对此变得越来越警惕:

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

You can use the andand gem, but I'm becoming more and more wary of it:

>> structure = { :a => { :b => 'foo' }} #=> {:a=>{:b=>"foo"}}
>> require 'andand' #=> true
>> structure[:a].andand[:b] #=> "foo"
>> structure[:c].andand[:b] #=> nil
半透明的墙 2024-11-06 19:53:48

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

更好的方法是创建一个新对象,其工作方式与 nil 非常相似,但支持此行为。使这个新对象成为哈希值的默认返回值。然后它就会起作用。

或者,您可以创建一个简单的“嵌套查找”函数,将散列和键传递给该函数,该函数按顺序遍历散列,并在可能时进行突破。

我个人更喜欢后两种方法之一。不过我认为如果第一个能够集成到 Ruby 语言中那就太好了。 (但是猴子补丁是一个坏主意。不要这样做。特别是不要展示你是一个多么酷的黑客。)

There is the cute but wrong way to do this. Which is to monkey-patch NilClass to add a [] method that returns nil. I say it is the wrong approach because you have no idea what other software may have made a different version, or what behavior change in a future version of Ruby can be broken by this.

A better approach is to create a new object that works a lot like nil but supports this behavior. Make this new object the default return of your hashes. And then it will just work.

Alternately you can create a simple "nested lookup" function that you pass the hash and the keys to, which traverses the hashes in order, breaking out when it can.

I would personally prefer one of the latter two approaches. Though I think it would be cute if the first was integrated into the Ruby language. (But monkey-patching is a bad idea. Don't do that. Particularly not to demonstrate what a cool hacker you are.)

囍孤女 2024-11-06 19:53:48

并不是说我会这样做,但你可以在 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 的答案。是的,它需要更多的打字,但那是因为它更安全。

Not that I would do it, but you can Monkeypatch in 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"

Go with @DigitalRoss's answer. Yes, it's more typing, but that's because it's safer.

╰◇生如夏花灿烂 2024-11-06 19:53:48

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

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

In my case, I needed a two-dimensional matrix where each cell is a list of items.

I found this technique which seems to work. It might work for the 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

The output is:

$ 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
剧终人散尽 2024-11-06 19:53:48

我目前正在尝试这个:

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

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文