Как избежать NoMethodError для отсутствующих элементов во вложенных хэшах, без повторных проверок nil?

Я ищу хороший способ избежать проверки nil на каждом уровне в глубоко вложенных хэшах. Например:

name = params[:company][:owner][:name] if params[:company] && params[:company][:owner] && params[:company][:owner][:name]

Это требует трех проверок и делает очень уродливый код. Любой способ обойти это?

Ответ 1

Ruby 2.3.0 ввел новый метод под названием dig как для Hash, так и для Array, который полностью решает эту проблему.

name = params.dig(:company, :owner, :name)

Он возвращает 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

Ответ 2

Лучший компромисс между функциональностью и ясностью IMO - это Raganwald andand. С этим вы бы сделали:

params[:company].andand[:owner].andand[:name]

Он похож на try, но в этом случае гораздо лучше читается, так как вы по-прежнему отправляете сообщения, как обычно, но с разделителем, который обращает внимание на то, что вы обрабатываете nils специально.

Ответ 3

Я не знаю, что вы хотите, но, может быть, вы могли бы это сделать?

name = params[:company][:owner][:name] rescue nil

Ответ 4

Эквивалентно второму решению, предложенному пользователем mpd, только более идиоматическому Ruby:

class Hash
  def deep_fetch *path
    path.inject(self){|acc, e| acc[e] if acc}
  end
end

hash = {a: {b: {c: 3, d: 4}}}

p hash.deep_fetch :a, :b, :c
#=> 3
p hash.deep_fetch :a, :b
#=> {:c=>3, :d=>4}
p hash.deep_fetch :a, :b, :e
#=> nil
p hash.deep_fetch :a, :b, :e, :f
#=> nil

Ответ 5

Если это рельсы, используйте

params.try(:[], :company).try(:[], :owner).try(:[], :name)

Ой, подождите, это еще более уродливое.; -)

Ответ 6

Если вы хотите попасть в monkeypatching, вы можете сделать что-то вроде этого


class NilClass
  def [](anything)
    nil
  end
end

Тогда вызов params[:company][:owner][:name] будет давать nil, если в любой точке один из вложенных хэшей равен nil.

EDIT: Если вам нужен более безопасный маршрут, который также обеспечивает чистый код, вы можете сделать что-то вроде


class Hash
  def chain(*args)
    x = 0
    current = self[args[x]]
    while current && x < args.size - 1
      x += 1
      current = current[args[x]]
    end
    current
  end
end

Код будет выглядеть так: params.chain(:company, :owner, :name)

Ответ 8

Я бы написал это как:

name = params[:company] && params[:company][:owner] && params[:company][:owner][:name]

Это не так чисто, как ? оператора в Io, но Ruby этого не имеет. Ответ @ThiagoSilveira также хорош, хотя он будет медленнее.

Ответ 9

Можете ли вы избежать использования многомерного хэша и использовать

params[[:company, :owner, :name]]

или

params[[:company, :owner, :name]] if params.has_key?([:company, :owner, :name])

вместо

Ответ 10

Напишите уродство один раз, затем скройте его

def check_all_present(hash, keys)
  current_hash = hash
  keys.each do |key|
    return false unless current_hash[key]
    current_hash = current_hash[key]
  end
  true
end

Ответ 11

делать:

params.fetch('company', {}).fetch('owner', {})['name']

Также на каждом шаге вы можете использовать соответствующий метод, встроенный в NilClass, чтобы выйти из nil, если это массив, строка или числовой. Просто добавьте to_hash в список этого списка и используйте его.

class NilClass; def to_hash; {} end end
params['company'].to_hash['owner'].to_hash['name']

Ответ 12

(Даже если это действительно старый вопрос, возможно, этот ответ будет полезен для некоторых людей stackoverflow, таких как я, которые не думали о выражении структуры управления "начать спасение".)

Я бы сделал это с помощью инструкции try catch (начать спасение на рубиновом языке):

begin
    name = params[:company][:owner][:name]
rescue
    #if it raises errors maybe:
    name = 'John Doe'
end

Ответ 13

Вам не нужен доступ к исходному хеш-определению - вы можете переопределить метод [] на лету после того, как вы его получите, используя h.instance_eval, например.

h = {1 => 'one'}
h.instance_eval %q{
  alias :brackets :[]
  def [] key
    if self.has_key? key
      return self.brackets(key)
    else
      h = Hash.new
      h.default = {}
      return h
    end
  end
}

Но это не поможет вам с кодом, который у вас есть, потому что вы полагаетесь на необоснованное значение, чтобы вернуть ложное значение (например, nil), и если вы делаете какой-либо из "нормальных" объектов авто-жизнеобеспечения выше вы получите пустой хеш для необоснованных значений, который оценивается как "истинный".

Вы можете сделать что-то вроде этого - он проверяет только определенные значения и возвращает их. Вы не можете установить их таким образом, потому что у нас нет возможности узнать, находится ли вызов в LHS назначения.

module AVHash
  def deep(*args)
    first = args.shift
    if args.size == 0
      return self[first]
    else
      if self.has_key? first and self[first].is_a? Hash
        self[first].send(:extend, AVHash)
        return self[first].deep(*args)
      else
        return nil
      end
    end
  end
end      

h = {1=>2, 3=>{4=>5, 6=>{7=>8}}}
h.send(:extend, AVHash)
h.deep(0) #=> nil
h.deep(1) #=> 2
h.deep(3) #=> {4=>5, 6=>{7=>8}}
h.deep(3,4) #=> 5
h.deep(3,10) #=> nil
h.deep(3,6,7) #=> 8

Опять же, вы можете только проверять значения с ним - не назначать их. Так что это не настоящая автожизнь, как мы все знаем, и любить ее в Perl.

Ответ 14

require 'xkeys' # on rubygems.org

params.extend XKeys::Hash # No problem that we got params from somebody else!
name = params[:company, :owner, :name] # or maybe...
name = params[:company, :owner, :name, :else => 'Unknown']
# Note: never any side effects for looking

# But you can assign too...
params[:company, :reviewed] = true

Ответ 15

Опасно, но работает:

class Object
        def h_try(key)
            self[key] if self.respond_to?('[]')
        end    
end

Мы можем new do

   user = { 
     :first_name => 'My First Name', 
     :last_name => 'my Last Name', 
     :details => { 
        :age => 3, 
        :birthday => 'June 1, 2017' 
      } 
   }

   user.h_try(:first_name) # 'My First Name'
   user.h_try(:something) # nil
   user.h_try(:details).h_try(:age) # 3
   user.h_try(:details).h_try(:nothing).h_try(:doesnt_exist) #nil

Цепочка "h_try" следует аналогичному стилю с цепочкой "try".

Ответ 16

Просто предложить один план на dig, попробуйте KeyDial драгоценный камень, который я написал. По сути, это обертка для dig но с той важной разницей, что она никогда не попадет в вас с ошибкой.

dig прежнему выдаст ошибку, если объект в цепочке имеет некоторый тип, который сам по себе не dig.

hash = {a: {b: {c: true}, d: 5}}

hash.dig(:a, :d, :c) #=> TypeError: Integer does not have #dig method

В этой ситуации dig не поможет, и вам нужно вернуться не только к hash[:a][:d].nil? && hash[:a][:d].nil? && но также проверяет hash[:a][:d].is_a?(Hash). KeyDial позволяет вам делать это без таких проверок или ошибок:

hash.call(:a, :d, :c) #=> nil
hash.call(:a, :b, :c) #=> true

Ответ 17

TL;DR; params&.dig(:company, :owner, :name)

Начиная с Ruby 2.3.0:

Вы также можете использовать &. называется "оператор безопасной навигации" как: params&.[](:company)&.[](:owner)&.[](:name). Это совершенно безопасно.

Использование dig для params самом деле небезопасно, так как params.dig потерпит неудачу, если params равен нулю.

Однако вы можете объединить эти два params&.dig(:company, :owner, :name): params&.dig(:company, :owner, :name).

Так что любое из следующего безопасно для использования:

params&.[](:company)&.[](:owner)&.[](:name)

params&.dig(:company, :owner, :name)