Accessing elements of nested hashes in ruby [dupli

2019-01-06 12:39发布

问题:

This question already has an answer here:

  • Ruby Style: How to check whether a nested hash element exists 14 answers
  • How to avoid NoMethodError for nil elements when accessing nested hashes? [duplicate] 4 answers

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.

回答1:

Traditionally, you really had to do something like this:

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

However, Ruby 2.3 added a feature 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.



回答2:

Ruby 2.3.0 introduced a new method called dig on both Hash and Array that solves this problem entirely.

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 use the ruby_dig gem or implement it 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


回答3:

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


回答4:

I made rubygem for this. Try vine.

Install:

gem install vine

Usage:

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


回答5:

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


回答6:

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


回答7:

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.



回答8:

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


回答9:

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


回答10:

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


回答11:

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


回答12:

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



回答13:

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.



回答14:

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


回答15:

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.