Доступ к элементам вложенных хэшей в Ruby [дубликат]
этот вопрос уже есть ответ здесь:
- Ruby Style: Как проверить, существует ли вложенный хэш-элемент 14 ответов
- Как избежать NoMethodError для нулевых элементов при доступе к вложенным хэшам? [дубликат] 4 ответы
Я работаю с небольшой утилитой, написанной на 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
. Это два эффекты:
- по умолчанию
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=>{}}}}
Я сделал 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
Решение 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
тогда просто используйте 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
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
.