Как избежать 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)
Ответ 7
Возможно, вы захотите изучить один из способов добавления авто-вивификации в рубиновые хэши. Существует ряд подходов, упомянутых в следующих потоках stackoverflow:
Ответ 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)