ruby on rails - rails - ¿Equivalente a.try() para un hash para evitar errores de "método indefinido" en nil?
ruby on rails tutorial (12)
Esta pregunta ya tiene una respuesta aquí:
En Rails podemos hacer lo siguiente en caso de que no exista un valor para evitar un error:
@myvar = @comment.try(:body)
¿Cuál es el equivalente cuando estoy profundizando en un hash y no quiero obtener un error?
@myvar = session[:comments][@comment.id]["temp_value"]
# [:comments] may or may not exist here
En el caso anterior, la session[:comments]try[@comment.id]
no funciona. ¿Qué haría?
A partir de Ruby 2.3 esto se vuelve un poco más fácil. En lugar de tener que anidar, try
declaraciones o defina su propio método, ahora puede usar Hash#dig
( #dig ).
h = { foo: {bar: {baz: 1}}}
h.dig(:foo, :bar, :baz) #=> 1
h.dig(:foo, :zot) #=> nil
O en el ejemplo de arriba:
session.dig(:comments, @comment.id, "temp_value")
Esto tiene el beneficio adicional de ser más como try
que algunos de los ejemplos anteriores. Si alguno de los argumentos lleva a que el hash devuelva nil, entonces responderá nil.
Cuando haces esto:
myhash[:one][:two][:three]
Estás encadenando un montón de llamadas a un método "[]", y el error ocurre si myhash [: uno] devuelve nil, porque nil no tiene un método []. Por lo tanto, una forma simple y bastante hacky es agregar un método [] a Niclass, que devuelve nil: lo configuraría en una aplicación de rieles de la siguiente manera:
Agrega el método:
#in lib/ruby_extensions.rb
class NilClass
def [](*args)
nil
end
end
Requerir el archivo:
#in config/initializers/app_environment.rb
require ''ruby_extensions''
Ahora puede invocar hashes anidados sin miedo: estoy demostrando en la consola aquí:
>> hash = {:foo => "bar"}
=> {:foo=>"bar"}
>> hash[:foo]
=> "bar"
>> hash[:doo]
=> nil
>> hash[:doo][:too]
=> nil
El uso apropiado de try with hash es @sesion.try(:[], :comments)
.
@session.try(:[], :comments).try(:[], commend.id).try(:[], ''temp_value'')
La respuesta de Andrew no funcionó para mí cuando intenté esto de nuevo recientemente. Tal vez algo ha cambiado?
@myvar = session[:comments].try(''[]'', @comment.id)
El ''[]''
está entre comillas en lugar de un símbolo :[]
La solución más hermosa es una vieja respuesta de Mladen Jablanović , ya que le permite profundizar en el hash más profundo de lo que podría con el uso de .try()
directas a .try()
, si quiere que el código se vea bien:
class Hash
def get_deep(*fields)
fields.inject(self) {|acc,e| acc[e] if acc}
end
end
Debe tener cuidado con varios objetos (especialmente params
), ya que las cadenas y matrices también responden a: [], pero el valor devuelto puede no ser el que desea, y Array plantea una excepción para cadenas o símbolos utilizados como índices.
Esa es la razón por la cual en la forma sugerida de este método (a continuación .is_a?(Hash)
se usa la prueba (usualmente fea) para .is_a?(Hash)
lugar de (normalmente mejor) .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
El último ejemplo generaría una excepción : "Símbolo como índice de matriz (TypeError)" si no estaba protegido por este feo "is_a? (Hash)".
Olvidaste poner un .
antes del try
:
@myvar = session[:comments].try(:[], @comment.id)
ya que []
es el nombre del método cuando lo hace [@comment.id]
.
Otro enfoque:
@myvar = session[:comments][@comment.id]["temp_value"] rescue nil
Esto también podría considerarse un poco peligroso porque puede ocultar demasiado, personalmente me gusta.
Si quieres más control, puedes considerar algo como:
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
Tratar de usar
@myvar = session[:comments][@comment.id]["temp_value"] if session[:comments]
supongamos que quiere encontrar params[:user][:email]
pero no está seguro de si el user
está allí en params
o no. Entonces-
puedes probar:
params[:user].try(:[], :email)
Devolverá nil
(si el user
no está allí o el email
no está en el user
) o el valor del email
en el user
.
El anuncio de Ruby 2.3.0-preview1 incluye una introducción al operador de navegación segura.
Un operador de navegación segura, que ya existe en C #, Groovy y Swift, se presenta para facilitar el manejo nulo como
obj&.foo
.Array#dig
yHash#dig
también se agregan.
Esto significa que a partir del código 2.3 a continuación
account.try(:owner).try(:address)
puede ser reescrito para
account&.owner&.address
Sin embargo, uno debe tener cuidado de que &
no sea una gota en reemplazo de #try
. Echale un vistazo a éste ejemplo:
> 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
También incluye un tipo de camino similar: Array#dig
y Hash#dig
. Entonces ahora esto
city = params.fetch(:[], :country).try(:[], :state).try(:[], :city)
puede ser reescrito para
city = params.dig(:country, :state, :city)
De nuevo, #dig
no está replicando el comportamiento de #try
. Así que ten cuidado con devolver los valores. Si params[:country]
devuelve, por ejemplo, un entero, TypeError: Integer does not have #dig method
generará.
Actualización: a partir de Ruby 2.3 use #dig
La mayoría de los objetos que responden a [] esperan un argumento entero, con Hash siendo una excepción que aceptará cualquier objeto (como cadenas o símbolos).
La siguiente es una versión ligeramente más robusta de la respuesta de Arsen7 que admite Array, Hash anidados, así como cualquier otro objeto que espere que un entero pase a [].
No es infalible, ya que alguien puede haber creado un objeto que implemente [] y no acepte un argumento entero. Sin embargo, esta solución funciona muy bien en el caso común, por ejemplo, extraer valores anidados de JSON (que tiene tanto Hash como 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
Se puede usar igual que la solución de Arsen7, pero también admite matrices, por ejemplo
json = { ''users'' => [ { ''name'' => { ''first_name'' => ''Frank''} }, { ''name'' => { ''first_name'' => ''Bob'' } } ] }
json.get_deep ''users'', 1, ''name'', ''first_name'' # Pulls out ''Bob''
@myvar = session.fetch(:comments, {}).fetch(@comment.id, {})["temp_value"]
Desde Ruby 2.0, puedes hacer:
@myvar = session[:comments].to_h[@comment.id].to_h["temp_value"]
Desde Ruby 2.3, puedes hacer:
@myvar = session.dig(:comments, @comment.id, "temp_value")