Доступ к элементам вложенных хэшей в 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
если: не является ключом в structure
, так далее.
15 ответов
В эти дни я обычно делаю так:
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
объект и достижим через default_proc
а также default_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
"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
, Это имеет два эффекта:
- Поведение по умолчанию для
recursive_hash[:foo]
такой же какrecursive_hash
, - Поведение по умолчанию для хэшей, созданных
recursive_hash[:foo]
"sdefault_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=>{}}}}
Традиционно вам действительно нужно было сделать что-то вроде этого:
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
самоцвет или реализовать его самостоятельно:
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
Я сделал для этого rubygem. Попробуйте лозу.
Установка:
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
Вы могли бы просто создать подкласс 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
s вместо хешей:
>> 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
Решение 1
Я предложил это в моем вопросе раньше:
class NilClass; def to_hash; {} end end
Hash#to_hash
уже определен и возвращает себя. Тогда вы можете сделать:
value = structure[:a].to_hash[:b]
to_hash
гарантирует, что вы получите пустой хеш при неудачном поиске предыдущего ключа.
Solution2
Это решение по духу похоже на слишком короткий ответ в том, что оно использует подкласс, но все же несколько иное. Если для определенного ключа нет значения, он не использует значение по умолчанию, а создает значение пустого хэша, чтобы у него не было проблемы путаницы в предположении, что ответ DigitalRoss имеет, как было указано мю слишком коротка
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 с самого начала и назначите значения ключа, он будет работать, но если вы захотите преобразовать хеш в экземпляр этого класса, это может стать проблемой.
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
Эта функция исправления обезьяны для 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
В моем случае мне нужна была двумерная матрица, где каждая ячейка представляет собой список элементов.
Я нашел эту технику, которая, кажется, работает. Это может работать для ОП:
$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
,
Вы можете использовать гем andand, но я все больше и больше настороженно отношусь к нему:
>> structure = { :a => { :b => 'foo' }} #=> {:a=>{:b=>"foo"}}
>> require 'andand' #=> true
>> structure[:a].andand[:b] #=> "foo"
>> structure[:c].andand[:b] #=> nil
Не то чтобы я это делал, но вы можете 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. Да, это больше печатать, но это потому, что это безопаснее.
Есть милый, но неправильный способ сделать это. Который должен обезьяна-патч NilClass
добавить []
метод, который возвращает nil
, Я говорю, что это неправильный подход, потому что вы понятия не имеете, какое другое программное обеспечение могло сделать другую версию, или какое изменение поведения в будущей версии Ruby может быть нарушено этим.
Лучший подход заключается в создании нового объекта, который работает так же, как nil
но поддерживает это поведение. Сделайте этот новый объект возвращением ваших хэшей по умолчанию. И тогда это будет просто работать.
В качестве альтернативы вы можете создать простую функцию "вложенного поиска", которой вы передаете хэш и ключи, которые пересекают хэши по порядку, прерываясь, когда это возможно.
Я бы лично предпочел один из последних двух подходов. Хотя я думаю, что было бы мило, если бы первый был интегрирован в язык Ruby. (Но исправление обезьян - плохая идея. Не делайте этого. Особенно, чтобы не продемонстрировать, какой вы крутой хакер.)