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


Итак, как бы вы проверили структуру хэша, особенно если это так вложенные глубоко (даже глубже, чем примеры, приведенные здесь)? Может быть, лучший вопрос: каков "рубиновый способ" сделать это?

643   14  

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)

другой вариант:

dino.fetch(:person, {})[:children]

можно использовать andand gem:

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

    Ничего не найдено.