Подтвердить что ты не робот

Доступ к элементам вложенных хэшей в рубине

Я работаю с небольшой утилитой, написанной на рубине, которая широко использует вложенные хеши. В настоящее время я проверяю доступ к вложенным хэш-элементам следующим образом:

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, если: a не является ключом в structure и т.д.

4b9b3361

Ответ 1

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

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

EDIT: Подробнее

Рубиновые хеши позволяют вам контролировать, как создаются значения по умолчанию при поиске нового ключа. Когда указано, это поведение инкапсулируется как объект Proc и доступно через default_proc и default_proc=. По умолчанию proc также можно указать, передав блок Hash.new.

Немного сломайте этот код. Это не идиоматический рубин, но проще разбить его на несколько строк:

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

Строка 1 объявляет переменную recursive_hash новой Hash и начинает блок recursive_hash 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=>{}}

Когда был создан хеш в recursive_hash[:foo], его default_proc был предоставлен recursive_hash default_proc. Это имеет два эффекта:

  • Поведение по умолчанию для recursive_hash[:foo] совпадает с положением recursive_hash.
  • Поведение по умолчанию для хэшей, созданных 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=>{}}}}

Ответ 2

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

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

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

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

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

Если вы используете версию Ruby старше 2.3, вы можете использовать гем ruby_dig или реализовать его самостоятельно:

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

Ответ 4

Я сделал рубигем для этого. Попробуйте vine.

Установка:

gem install vine

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

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

Ответ 5

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

Решение 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

Он отклоняется от спецификации, указанной в вопросе. Когда задан ключ undefined, он вернет пустое хэш-сообщение nil.

p structure[:c] # => {}

Если вы создаете экземпляр этого NilFreeHash с самого начала и назначаете значения ключа, он будет работать, но если вы хотите преобразовать хэш в экземпляр этого класса, это может быть проблемой.

Ответ 8

Вы могли бы просто построить подкласс 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

Ответ 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

Эта функция патчей для хэша должна быть проще (по крайней мере для меня). Он также не изменяет структуру, то есть меняет 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

Ответ 11

Вы можете использовать andand gem, но я все больше и больше опасаюсь:

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

Ответ 12

Есть милый, но неправильный способ сделать это. Что нужно для monkey-patch NilClass, чтобы добавить метод [], который возвращает nil. Я говорю, что это неправильный подход, потому что вы не представляете, какое другое программное обеспечение могло бы сделать другую версию, или какое изменение поведения в будущей версии Ruby может быть нарушено этим.

Лучшим подходом является создание нового объекта, который очень похож на nil, но поддерживает это поведение. Сделайте этот новый объект стандартным возвратом ваших хэшей. И тогда это будет просто работать.

В качестве альтернативы вы можете создать простую функцию "вложенного поиска", через которую вы передаете хеш и ключи, которые перемещаются по хэшам в порядке, вырываясь, когда это возможно.

Я лично предпочел бы один из последних двух подходов. Хотя я думаю, что это было бы мило, если бы первый был интегрирован в язык Ruby. (Но обезьяна-паттинг - плохая идея. Не делайте этого. В частности, чтобы не продемонстрировать, какой вы классный хакер.)

Ответ 13

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

Ответ 14

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

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

Ответ 15

В настоящее время я тестирую это:

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