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