Стиль Ruby: как проверить, существует ли вложенный элемент хэша
Рассмотрим "человека", хранящегося в хеше. Два примера:
fred = {:person => {:name => "Fred", :spouse => "Wilma", :children => {:child => {:name => "Pebbles"}}}}
slate = {:person => {:name => "Mr. Slate", :spouse => "Mrs. Slate"}}
Если "человек" не имеет детей, элемент "children" отсутствует. Итак, для мистера Слейта мы можем проверить, есть ли у него родители:
slate_has_children = !slate[:person][:children].nil?
Итак, что, если мы не знаем, что "шифер" - это "хэши" человека? Рассмотрим:
dino = {:pet => {:name => "Dino"}}
Мы больше не можем легко проверить детей:
dino_has_children = !dino[:person][:children].nil?
NoMethodError: undefined method `[]' for nil:NilClass
Итак, как бы вы проверили структуру хэша, особенно если он глубоко вложен (даже глубже, чем примеры, приведенные здесь)? Возможно, лучший вопрос: что такое "Ruby way"?
Ответы
Ответ 1
Самый очевидный способ сделать это - просто проверить каждый шаг:
has_children = slate[:person] && slate[:person][:children]
Использование .nil? действительно требуется, только если вы используете false как значение-заполнителя, и на практике это редко. Обычно вы можете просто протестировать его.
Обновление. Если вы используете Ruby 2.3 или более поздней версии, существует встроенный dig
метод, который делает то, что описано в этом ответе.
Если нет, вы также можете определить свой собственный метод "копания" хэша, который может существенно упростить это:
class Hash
def dig(*path)
path.inject(self) do |location, key|
location.respond_to?(:keys) ? location[key] : nil
end
end
end
Этот метод проверяет каждый шаг пути и избегает отключения при звонках на ноль. Для неглубоких структур полезность несколько ограничена, но для глубоко вложенных структур я считаю ее неоценимой:
has_children = slate.dig(:person, :children)
Вы также можете сделать это более надежным, например, тестировать, действительно ли заполняется запись: children:
children = slate.dig(:person, :children)
has_children = children && !children.empty?
Ответ 2
С Ruby 2.3 у нас будет поддержка безопасного навигатора:
https://www.ruby-lang.org/en/news/2015/11/11/ruby-2-3-0-preview1-released/
has_children
теперь можно записать как:
has_children = slate[:person]&.[](:children)
dig
:
has_children = slate.dig(:person, :children)
Ответ 3
Другая альтернатива:
dino.fetch(:person, {})[:children]
Ответ 4
Вы можете использовать камень andand
:
require 'andand'
fred[:person].andand[:children].nil? #=> false
dino[:person].andand[:children].nil? #=> true
Дополнительные пояснения можно найти на http://andand.rubyforge.org/.
Ответ 5
Можно использовать хэш со значением по умолчанию {} - пустой хеш. Например,
dino = Hash.new({})
dino[:pet] = {:name => "Dino"}
dino_has_children = !dino[:person][:children].nil? #=> false
Это работает с уже созданным Hash:
dino = {:pet=>{:name=>"Dino"}}
dino.default = {}
dino_has_children = !dino[:person][:children].nil? #=> false
Или вы можете определить метод [] для класса nil
class NilClass
def [](* args)
nil
end
end
nil[:a] #=> nil
Ответ 6
Традиционно вам действительно нужно было сделать что-то вроде этого:
structure[:a] && structure[:a][:b]
Однако Ruby 2.3 добавил функцию, которая делает этот способ более грациозным:
structure.dig :a, :b # nil if it misses anywhere along the way
Существует драгоценный камень, называемый ruby_dig
, который будет исправлять это для вас.
Ответ 7
dino_has_children = !dino.fetch(person, {})[:children].nil?
Обратите внимание, что в рельсах вы также можете:
dino_has_children = !dino[person].try(:[], :children).nil? #
Ответ 8
Здесь вы можете сделать глубокую проверку любых значений фальши в хеше и любых вложенных хэшах без обезьяны, исправляющих класс Ruby Hash (ПОЖАЛУЙСТА, не используйте патч для обезьян на классах Ruby, то вы не должны этого делать, КОГДА-ЛИБО).
(Предполагая Rails, хотя вы можете легко изменить это для работы за пределами Rails)
def deep_all_present?(hash)
fail ArgumentError, 'deep_all_present? only accepts Hashes' unless hash.is_a? Hash
hash.each do |key, value|
return false if key.blank? || value.blank?
return deep_all_present?(value) if value.is_a? Hash
end
true
end
Ответ 9
Упрощение ответов выше здесь:
Создайте метод рекурсивного хеширования, значение которого не может быть равно нулю, как показано ниже.
def recursive_hash
Hash.new {|key, value| key[value] = recursive_hash}
end
> slate = recursive_hash
> slate[:person][:name] = "Mr. Slate"
> slate[:person][:spouse] = "Mrs. Slate"
> slate
=> {:person=>{:name=>"Mr. Slate", :spouse=>"Mrs. Slate"}}
slate[:person][:state][:city]
=> {}
Если вы не возражаете против создания пустых хэшей, если значение не существует для ключа :)
Ответ 10
def flatten_hash(hash)
hash.each_with_object({}) do |(k, v), h|
if v.is_a? Hash
flatten_hash(v).map do |h_k, h_v|
h["#{k}_#{h_k}"] = h_v
end
else
h[k] = v
end
end
end
irb(main):012:0> fred = {:person => {:name => "Fred", :spouse => "Wilma", :children => {:child => {:name => "Pebbles"}}}}
=> {:person=>{:name=>"Fred", :spouse=>"Wilma", :children=>{:child=>{:name=>"Pebbles"}}}}
irb(main):013:0> slate = {:person => {:name => "Mr. Slate", :spouse => "Mrs. Slate"}}
=> {:person=>{:name=>"Mr. Slate", :spouse=>"Mrs. Slate"}}
irb(main):014:0> flatten_hash(fred).keys.any? { |k| k.include?("children") }
=> true
irb(main):015:0> flatten_hash(slate).keys.any? { |k| k.include?("children") }
=> false
Это сгладит все хеши в одном, а затем в любом? возвращает true, если существует какая-либо клавиша, соответствующая подстроке "дети".
Это также может помочь.
Ответ 11
Вы можете попробовать играть с помощью
dino.default = {}
Или, например:
empty_hash = {}
empty_hash.default = empty_hash
dino.default = empty_hash
Таким образом вы можете позвонить
empty_hash[:a][:b][:c][:d][:e] # and so on...
dino[:person][:children] # at worst it returns {}
Ответ 12
Учитывая
x = {:a => {:b => 'c'}}
y = {}
вы можете проверить x и y следующим образом:
(x[:a] || {})[:b] # 'c'
(y[:a] || {})[:b] # nil
Ответ 13
Thks @tadman для ответа.
Для тех, кто хочет perfs (и застрял с ruby < 2.3), этот метод в 2,5 раза быстрее:
unless Hash.method_defined? :dig
class Hash
def dig(*path)
val, index, len = self, 0, path.length
index += 1 while(index < len && val = val[path[index]])
val
end
end
end
и если вы используете RubyInline, этот метод будет в 16 раз быстрее:
unless Hash.method_defined? :dig
require 'inline'
class Hash
inline do |builder|
builder.c_raw '
VALUE dig(int argc, VALUE *argv, VALUE self) {
rb_check_arity(argc, 1, UNLIMITED_ARGUMENTS);
self = rb_hash_aref(self, *argv);
if (NIL_P(self) || !--argc) return self;
++argv;
return dig(argc, argv, self);
}'
end
end
end
Ответ 14
Вы также можете определить модуль для псевдонимов методов скобок и использовать синтаксис Ruby для чтения/записи вложенных элементов.
ОБНОВЛЕНИЕ: вместо того, чтобы переопределять вспомогательные устройства для скобок, запросите экземпляр Hash для расширения модуля.
module Nesty
def []=(*keys,value)
key = keys.pop
if keys.empty?
super(key, value)
else
if self[*keys].is_a? Hash
self[*keys][key] = value
else
self[*keys] = { key => value}
end
end
end
def [](*keys)
self.dig(*keys)
end
end
class Hash
def nesty
self.extend Nesty
self
end
end
Затем вы можете сделать:
irb> a = {}.nesty
=> {}
irb> a[:a, :b, :c] = "value"
=> "value"
irb> a
=> {:a=>{:b=>{:c=>"value"}}}
irb> a[:a,:b,:c]
=> "value"
irb> a[:a,:b]
=> {:c=>"value"}
irb> a[:a,:d] = "another value"
=> "another value"
irb> a
=> {:a=>{:b=>{:c=>"value"}, :d=>"another value"}}
Ответ 15
Я не знаю, как это "Ruby" (!), Но гем KeyDial, который я написал, позволяет вам делать это в основном без изменения исходного синтаксиса:
has_kids = !dino[:person][:children].nil?
будет выглядеть так:
has_kids = !dino.dial[:person][:children].call.nil?
Это использует некоторую хитрость для промежуточных вызовов доступа к ключу. При call
он попытается dig
предыдущие ключи на dino
, а если обнаружит ошибку (как и будет), вернет nil. nil?
тогда, конечно, возвращает истину.
Ответ 16
Вы можете использовать комбинацию &
и key?
это O (1) по сравнению с dig
что O (n), и это гарантирует, что человек определен без NoMethodError: undefined method '[]' for nil:NilClass
fred[:person]&.key?(:children) //=>true
slate[:person]&.key?(:children)