2010-12-06 1 views
24

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

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

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

+1

В заводной вы должны использовать `` оператор?. На самом деле меня интересует эквивалентный оператор. Вы можете расширить класс хэша и добавить оператора. – Pasta 2010-12-06 22:54:34

+0

@Pasta [Io] (http://iolanguage.com) имеет аналогичный оператор, но Ruby этого не делает. – Phrogz 2010-12-06 23:19:43

+2

@JesseSielaff Первоначально это задано в 2010 году. У него 13 ответов (у другого есть 5), больше голосов, больше избранных и немного отличается. Не следует ли дублировать вопрос по другому вопросу? – 2016-01-06 02:42:58

ответ

25

Рубин 2.3.0 введен a new method called 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 
3

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

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

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

+0

Я бы не сказал, что это уродливее. Спасибо за ответ Кайл. – 2010-12-07 00:25:11

6

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

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

извините, что, но беспорядочные спасания являются злыми, вы можете замаскировать так много несвязанных ошибок ... – tokland 2010-12-06 23:25:04

3

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

 
class NilClass 
    def [](anything) 
    nil 
    end 
end 

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

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)

10

Лучший компромисс между функциональностью и ясности ИМО Raganwald's andand. При том, что вы могли бы сделать:

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

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

2

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

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

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

1

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

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

или

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

вместо Вы?

1

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

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 
0

Вам не нужно доступ к исходному хэш-определению - вы можете переопределить метод [] на лету, после того как вы получите его с помощью 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 
} 

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

Вы можете сделать что-то вроде этого - он проверяет только определенные значения и возвращает их. Вы не можете установить их таким образом, потому что у нас нет возможности узнать, находится ли вызов в 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.

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 
1

Do:

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'] 
1

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

Я хотел бы сделать это с попытки поймать заявление (начать спасение в языке Ruby):

begin 
    name = params[:company][:owner][:name] 
rescue 
    #if it raises errors maybe: 
    name = 'John Doe' 
end 
0
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 
0

Dangerous, но работает:

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

Мы можем новое сделать

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" цепь следующим же стиле цепь «попробуйте».

0

Как Руби 2.3.0:

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

Использование является не безопасно, как params.dig(:company, :owner, :name) потерпит неудачу, если params равна нулю.

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

Так одно из следующих действий является безопасным для использования:

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

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

Смежные вопросы