Стиль 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?

Ответ 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)