Ruby Style: Как проверить, существует ли вложенный хэш-элемент
рассмотрим "человека", хранящегося в хэше. Вот два примера:
fred = {:person => {:name => "Fred", :spouse => "Wilma", :children => {:child => {:name => "Pebbles"}}}}
slate = {:person => {:name => "Mr. Slate", :spouse => "Mrs. Slate"}}
если у " человека "нет детей, то элемент" дети " отсутствует. Итак, для мистера Слейта мы можем проверить, есть ли у него родители:
slate_has_children = !slate[:person][:children].nil?
Итак, что, если мы не знаем, что "шифер" - это хэш "человек"? Рассмотрим:
dino = {:pet => {:name => "Dino"}}
мы не можем легко проверить больше для детей:
dino_has_children = !dino[:person][:children].nil?
NoMethodError: undefined method `[]' for nil:NilClass
Итак, как бы вы проверили структуру хэша, особенно если это так вложенные глубоко (даже глубже, чем примеры, приведенные здесь)? Может быть, лучший вопрос: каков "рубиновый способ" сделать это?
14 ответов:
самый очевидный способ сделать это-просто проверить каждый шаг пути:
has_children = slate[:person] && slate[:person][:children]использование .Нил? действительно требуется только при использовании false в качестве значения заполнителя, и на практике это редко. Как правило, вы можете просто проверить его существование.
обновление: если вы используете Ruby 2.3 или более поздней версии есть встроенный
digметод, который делает то, что описано в этом ответе.если нет, вы также можете определите свой собственный хэш-метод "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?
С 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)
можно использовать
andandgem:require 'andand' fred[:person].andand[:children].nil? #=> false dino[:person].andand[:children].nil? #=> trueвы можете найти дополнительные объяснения в http://andand.rubyforge.org/.
можно использовать хэш со значением по умолчанию {} - пустой хэш. Например,
dino = Hash.new({}) dino[:pet] = {:name => "Dino"} dino_has_children = !dino[:person][:children].nil? #=> falseэто работает и с уже созданным хэшем:
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
традиционно, вы действительно должен сделать что-то вроде этого:
structure[:a] && structure[:a][:b]однако Ruby 2.3 добавил функцию, которая делает этот способ более изящным:
structure.dig :a, :b # nil if it misses anywhere along the wayесть драгоценный камень под названием
ruby_digэто будет обратно-патч это для вас.
dino_has_children = !dino.fetch(person, {})[:children].nil?обратите внимание, что в Rails вы также можете сделать:
dino_has_children = !dino[person].try(:[], :children).nil? #
вот способ, которым вы можете сделать глубокую проверку на любые ложные значения в хэше и любые вложенные хэши без исправления обезьяны класса Ruby Hash (пожалуйста, не обезьяна патч на рубиновых классов, это то, что вы не должны делать, никогда).
(предполагая рельсы, хотя вы можете легко изменить это, чтобы работать вне рельсов)
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
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, если существует какой-либо ключ, соответствующий подстроке "children". Это тоже может помочь.
упрощение приведенных выше ответов здесь:
создайте рекурсивный хэш-метод, значение которого не может быть равно нулю, как показано ниже.
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] => {}Если вы не против создания пустых хэшей, если значение не существует для ключа:)
вы можете попробовать играть с
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 {}
дано
x = {:a => {:b => 'c'}} y = {}вы можете проверить x и y такой:
(x[:a] || {})[:b] # 'c' (y[:a] || {})[:b] # nil
Thks @tadman для ответа.
для тех, кто хочет perfs (и застрял с ruby
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
вы также можете определить модуль для псевдонима методов скобок и использовать синтаксис Ruby для чтения/записи вложенных элементов.
UPDATE: вместо переопределения методов доступа к скобкам запросите экземпляр хэша для расширения модуля.
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"}}
Comments