Доступ к элементам вложенных хэшей в 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]

и вам nil if: a не является ключом в structure, etc.

15 ответов


традиционно, вы действительно должен сделать что-то вроде этого:

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

однако Ruby 2.3 добавил функцию, которая делает этот путь более изящным:

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

есть камень под названием ruby_dig что будет патч этот для вас.


Ruby 2.3.0 представлен новый метод, называемый dig на Hash и Array, что полностью решает эту проблему.

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

возвращает nil если ключ отсутствует на любом уровне.

если вы используете версию Ruby старше 2.3, вы можете использовать ruby_dig 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

кстати, я обычно делаю это в эти дни:

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

это даст вам хэш, который создает новый хэш как запись для отсутствующего ключа, но возвращает ноль для второго уровня ключа:

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 по умолчанию. Таким образом, хэш, созданный как значение по умолчанию при поиске невидимого ключа, будет иметь то же поведение по умолчанию.

EDIT: Подробнее

рубиновые хэши позволяют управлять тем, как создаются значения по умолчанию при поиске нового ключа. Если указано, это поведение инкапсулируется как Proc объект и доступен через default_proc и default_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_hash ' s default_proc. Блок передается двумя объектами:h, который является Hash экземпляр поиск ключа выполняется на, и k, ключ рассматривается вверх.

строка 2 устанавливает значение по умолчанию в хэше на новое Hash экземпляра. Поведение по умолчанию для этого хэша предоставляется путем передачи Proc создано из файла 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_proc был поставлен recursive_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=>{}}}}

Я сделал rubygem для этого. Попробуй!--3-->виноград.

установка:

gem install vine

использование:

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

Я думаю, что одним из наиболее читаемых решений является использование 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 гарантирует, что вы получите пустой хэш при сбое предыдущего поиска ключа.

Solution2

это решение похоже по духу на mu слишком короткий ответ в том, что он использует подкласс, но все же несколько отличается. В случае нет значения для определенного ключа, он не использует значение по умолчанию, а создает значение пустого хэша, так что у него нет проблемы путаницы в назначении, что ответ 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 из начиная и назначая значения ключей, он будет работать, но если вы хотите преобразовать хэш в экземпляр этого класса, это может быть проблемой.


вы можете просто построить хеш-подкласс с дополнительным вариадическим методом для копания полностью с соответствующими проверками по пути. Что-то вроде этого (с лучшим именем, конечно):

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

тогда просто используйте Things вместо хешей:

>> 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 джем, но я становлюсь все более и более осторожным:

>> 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. (Но латать обезьян-плохая идея. Не делай этого. Особенно не для того, чтобы продемонстрировать, какой ты крутой хакер являются.)


не то, чтобы я это сделал, но вы можете 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. Да, он больше печатает, но это потому, что так безопаснее.


в моем случае мне нужна двумерная Матрица, где каждая ячейка представляет собой список элементов.

Я нашел эту технику, которая, кажется, работает. Это может сработать для 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.