Эквивалент .try () для хэша, чтобы избежать ошибок «неопределенного метода» для nil? [Дубликат]

134

В Rails мы можем сделать следующее, если значение не существует, чтобы избежать ошибки:

@myvar = @comment.try(:body)

Что эквивалентно, когда я копаю глубоко в хэш и не хочу получать ошибку?

@myvar = session[:comments][@comment.id]["temp_value"] 
# [:comments] may or may not exist here

В приведенном выше случае session[:comments]try[@comment.id] не работает. Что бы?

  • 2
    Похожий вопрос: stackoverflow.com/questions/4371716/…
  • 4
    В Ruby 2.3 появился Hash#dig который делает здесь ненужную try . У @baxang лучший ответ сейчас.
Теги:
hash
ruby-on-rails-3

12 ответов

217
Лучший ответ

Вы забыли поставить . перед try:

@myvar = session[:comments].try(:[], @comment.id)

поскольку [] - это имя метода, когда вы выполняете [@comment.id].

  • 0
    Это дает мне ошибку. No method name given. Это начало цикла: unless session[:profile_activity].try[@profile.id]
  • 0
    @sscirrus: Извините, моя ошибка. Я отредактировал свой ответ соответственно. Попробуйте новый ответ.
Показать ещё 4 комментария
45

Объявление Ruby 2.3.0-preview1 включает введение безопасного навигатора.

Безопасный навигационный оператор, который уже существует в С#, Groovy и Swift, предназначен для упрощения обработки nil как obj&.foo. Array#dig и Hash#dig.

Это означает, что в 2.3 ниже кода

account.try(:owner).try(:address)

можно переписать на

account&.owner&.address

Однако следует быть осторожным, что & не является заменой #try. Взгляните на этот пример:

> params = nil
nil
> params&.country
nil
> params = OpenStruct.new(country: "Australia")
#<OpenStruct country="Australia">
> params&.country
"Australia"
> params&.country&.name
NoMethodError: undefined method `name' for "Australia":String
from (pry):38:in `<main>'
> params.try(:country).try(:name)
nil

Он также включает аналогичный путь: Array#dig и Hash#dig. Итак, теперь это

city = params.fetch(:[], :country).try(:[], :state).try(:[], :city)

можно переписать на

city = params.dig(:country, :state, :city)

Опять же, #dig не воспроизводит поведение #try. Поэтому будьте осторожны с возвращаемыми значениями. Если params[:country] возвращает, например, значение Integer, TypeError: Integer does not have #dig method.

  • 9
    ломает, если хеш nil для тех, кто, где интересно
  • 2
    Обратите внимание, что на самом деле это не работает с сеансом Rails, так как ActionDispatch :: Request :: Session не реализует #dig
Показать ещё 2 комментария
20

Самое прекрасное решение - это старый ответ Младен Ябланович, поскольку он позволяет вам копать в хэш глубже, чем вы могли с помощью прямых вызовов .try() если вы хотите, чтобы код по-прежнему выглядел красиво:

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

Вы должны быть осторожны с различными объектами (особенно params), потому что строки и массивы также отвечают на: [], но возвращаемое значение может быть не таким, как вы хотите, и Array вызывает исключение для строк или символов, используемых в качестве индексов.

Вот почему в предложенной форме этого метода (см. ниже) вместо (обычно лучше) используется (обычно уродливый) тест для .is_a?(Hash) .respond_to?(:[]):

class Hash
  def get_deep(*fields)
    fields.inject(self) {|acc,e| acc[e] if acc.is_a?(Hash)}
  end
end

a_hash = {:one => {:two => {:three => "asd"}, :arr => [1,2,3]}}

puts a_hash.get_deep(:one, :two               ).inspect # => {:three=>"asd"}
puts a_hash.get_deep(:one, :two, :three       ).inspect # => "asd"
puts a_hash.get_deep(:one, :two, :three, :four).inspect # => nil
puts a_hash.get_deep(:one, :arr            ).inspect    # => [1,2,3]
puts a_hash.get_deep(:one, :arr, :too_deep ).inspect    # => nil

В последнем примере возникло бы исключение: "Символ как индекс массива (TypeError)", если он не был защищен этим уродливым "is_a? (Hash)".

  • 1
    на самом деле, поскольку nil не является Hash вы, вероятно, можете упростить до fields.inject(self) {|acc,e| acc[e] if acc.is_a?(Hash)} Но у меня есть ощущение, что #respond_to будет лучше.
  • 1
    @riffraff: Вы совершенно правы в этом отношении acc & acc.is_a?() - считаете это ошибкой ;-). Но respond_to не сработает, потому что String и многие другие объекты также отвечают на :[] , но результат этого метода не тот, что здесь требуется.
Показать ещё 2 комментария
13

Правильное использование try с хешем @sesion.try(:[], :comments).

@session.try(:[], :comments).try(:[], commend.id).try(:[], 'temp_value')
  • 0
    -1 Почему это не может быть вложено? try применяется к любому Object , а nil является Object , поэтому я подозреваю, что сработает следующее: nil.try(:do).try(:do_not).try(:there_is_a_try) .
  • 2
    «Не может быть вложенным» неправильно. Но для вашего конкретного случая моя оценка была правильной. то, что вам нужно сделать, это использовать try with: [], для использования его непосредственно с ключом, вам нужно использовать fetch.
10

Обновление: По состоянию на Ruby 2.3 используйте # dig

Большинство объектов, которые отвечают на [], ожидают аргумент Integer, причем Hash является исключением, которое примет любой объект (например, строки или символы).

Ниже приведен немного более надежный вариант ответа Arsen7, который поддерживает вложенные Array, Hash, а также любые другие объекты, которые ожидают, что Integer передан в [].

Это не безупречное доказательство, поскольку кто-то может создать объект, который реализует [] и не принимает аргумент Integer. Однако это решение отлично работает в общем случае, например. вытягивание вложенных значений из JSON (имеющего как Hash, так и Array):

class Hash
  def get_deep(*fields)
    fields.inject(self) { |acc, e| acc[e] if acc.is_a?(Hash) || (e.is_a?(Integer) && acc.respond_to?(:[])) }
  end
end

Его можно использовать так же, как решение Arsen7, но также поддерживает массивы, например.

json = { 'users' => [ { 'name' => { 'first_name' => 'Frank'} }, { 'name' => { 'first_name' => 'Bob' } } ] }

json.get_deep 'users', 1, 'name', 'first_name' # Pulls out 'Bob'
10
@myvar = session.fetch(:comments, {}).fetch(@comment.id, {})["temp_value"]

Из Ruby 2.0 вы можете сделать:

@myvar = session[:comments].to_h[@comment.id].to_h["temp_value"]

Из Ruby 2.3 вы можете сделать:

@myvar = session.dig(:comments, @comment.id, "temp_value")
  • 1
    потому что это не то, что делает .try .
  • 3
    Похоже, что сессия не реализует dig: неопределенный метод `dig 'для # <ActionDispatch :: Request :: Session: 0x007ffc6cafa698>
8

скажите, что вы хотите найти params[:user][:email], но не уверены, есть ли user в params или нет. Тогда -

вы можете попробовать:

params[:user].try(:[], :email)

Он вернет либо nil (если user не существует, либо email не существует в user), либо иначе значение email в user.

6

Другой подход:

@myvar = session[:comments][@comment.id]["temp_value"] rescue nil

Это также может считаться немного опасным, потому что он может скрыть слишком много, лично мне это нравится.

Если вы хотите большего контроля, вы можете рассмотреть что-то вроде:

def handle # just an example name, use what speaks to you
    raise $! unless $!.kind_of? NoMethodError # Do whatever checks or 
                                              # reporting you want
end
# then you may use
@myvar = session[:comments][@comment.id]["temp_value"] rescue handle
5

По сравнению с Ruby 2.3 это становится немного проще. Вместо того, чтобы вставлять операторы try или определять свой собственный метод, вы можете теперь использовать Hash#dig (документацию).

h = { foo: {bar: {baz: 1}}}

h.dig(:foo, :bar, :baz)           #=> 1
h.dig(:foo, :zot)                 #=> nil

Или в приведенном выше примере:

session.dig(:comments, @comment.id, "temp_value")

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

  • 0
    Не могу использовать dig для хеша сессии
2

Когда вы это сделаете:

myhash[:one][:two][:three]

Вы просто связываете кучу вызовов с методом "[]", возникает ошибка, если myhash [: one] возвращает nil, потому что nil не имеет метода []. Таким образом, один простой и довольно хакерский способ заключается в добавлении метода [] к Niclass, который возвращает nil: я бы установил это в приложении rails следующим образом:

Добавьте метод:

#in lib/ruby_extensions.rb
class NilClass
  def [](*args)
    nil
  end
end

Требовать файл:

#in config/initializers/app_environment.rb
require 'ruby_extensions'

Теперь вы можете вызывать вложенные хэши без страха: я демонстрирую на консоли здесь:

>> hash = {:foo => "bar"}
=> {:foo=>"bar"}
>> hash[:foo]
=> "bar"
>> hash[:doo]
=> nil
>> hash[:doo][:too]
=> nil
  • 0
    Это увлекательно - спасибо Макс! Есть ли у этого недостатки, о которых вы знаете? У кого-нибудь еще есть взгляд на это?
  • 13
    Это скроет ваши проблемы с неожиданными nils в других частях вашего кода. Я бы посчитал этот метод опасным.
1

Эндрю ответ не работал у меня, когда я пробовал это снова недавно. Может, что-то изменилось?

@myvar = session[:comments].try('[]', @comment.id)

'[]' находится в кавычках вместо символа :[]

-1

Попробуйте использовать

@myvar = session[:comments][@comment.id]["temp_value"] if session[:comments]
  • 0
    как насчет того, если я не знаю, существует ли [:comments] или [@comment.id] ?
  • 0
    в этом случае я думаю, что было бы лучше создать вложенные операторы IF для проверки каждого параметра в сеансе
Показать ещё 2 комментария

Ещё вопросы

Сообщество Overcoder
Наверх
Меню