ruby-on-rails - example - ruby array
¿Cómo evitar NoMethodError para elementos faltantes en hash anidados, sin repetidos controles nulos? (16)
Estoy buscando una buena manera de evitar la comprobación de nil
en cada nivel en hashes profundamente anidados. Por ejemplo:
name = params[:company][:owner][:name] if params[:company] && params[:company][:owner] && params[:company][:owner][:name]
Esto requiere tres comprobaciones y crea un código muy feo. ¿Alguna forma de evitar esto?
¿Eres capaz de evitar usar un hash multidimensional y usar
params[[:company, :owner, :name]]
o
params[[:company, :owner, :name]] if params.has_key?([:company, :owner, :name])
¿en lugar?
(Aunque es una pregunta muy antigua, tal vez esta respuesta sea útil para algunas personas con como yo que no pensaron en la expresión de estructura de control "begin rescue").
Lo haría con una declaración de catch try (comenzar rescate en lenguaje ruby):
begin
name = params[:company][:owner][:name]
rescue
#if it raises errors maybe:
name = ''John Doe''
end
A partir de Ruby 2.3.0:
También puedes usar &.
llamado "operador de navegación segura" como: params&.[](:company)&.[](:owner)&.[](:name)
. Este es perfectamente seguro.
Usar dig
no es seguro ya que params.dig(:company, :owner, :name)
fallará si params
es nil.
Sin embargo, puede combinar los dos como: params&.dig(:company, :owner, :name)
.
Por lo tanto, cualquiera de los siguientes es seguro de usar:
params&.[](:company)&.[](:owner)&.[](:name)
params&.dig(:company, :owner, :name)
El mejor compromiso entre la funcionalidad y la claridad IMO es Randwald''s and and. Con eso, harías:
params[:company].andand[:owner].andand[:name]
Es similar a try
, pero en este caso se lee mucho mejor, ya que todavía está enviando mensajes como normal, pero con un delimitador entre eso llama la atención sobre el hecho de que está tratando nils especialmente.
Equivalente a la segunda solución que sugirió el usuario mpd
, solo Ruby más idiomática:
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
Es posible que desee buscar en una de las formas de agregar auto-vivification a los hashes de ruby. Hay una serie de enfoques mencionados en los siguientes subprocesos :
Escribe la fealdad una vez, luego ocúltala
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
Hacer:
params.fetch(''company'', {}).fetch(''owner'', {})[''name'']
También en cada paso, puede usar un método apropiado construido en NilClass
para escapar de nil
, si fuera una matriz, cadena o numérico. Simplemente agregue to_hash
al inventario de esta lista y to_hash
.
class NilClass; def to_hash; {} end end
params[''company''].to_hash[''owner''].to_hash[''name'']
No necesita acceder a la definición del hash original: puede anular el método [] sobre la marcha después de obtenerlo utilizando h.instance_eval, por ej.
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
}
Pero eso no te ayudará con el código que tienes, porque estás confiando en un valor no encontrado para devolver un valor falso (por ejemplo, nada) y si haces alguna de las cosas de auto-vivificación "normal" vinculadas a lo anterior, Va a terminar con un hash vacío para valores no encontrados, que se evalúa como "verdadero".
Podría hacer algo como esto: solo busca valores definidos y los devuelve. No puede configurarlos de esta manera, porque no tenemos forma de saber si la llamada está en el LHS de una tarea.
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
De nuevo, sin embargo, solo puedes verificar valores con él, no asignarlos. Entonces no es una auto-vivificación real, como todos sabemos y nos encanta en Perl.
No sé si eso es lo que quieres, pero ¿quizás podrías hacer esto?
name = params[:company][:owner][:name] rescue nil
Peligroso pero funciona:
class Object
def h_try(key)
self[key] if self.respond_to?(''[]'')
end
end
Podemos hacer nuevas
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
La cadena "h_try" sigue un estilo similar a una cadena "try".
Ruby 2.3.0 introdujo un nuevo método llamado dig
en ambos Hash
y Array
que resuelve este problema por completo.
name = params.dig(:company, :owner, :name)
Devuelve nil
si la clave falta en cualquier nivel.
Si está usando una versión de Ruby anterior a 2.3, puede usar la gema ruby_dig o implementarla usted mismo:
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
Si es rieles, usa
params.try(:[], :company).try(:[], :owner).try(:[], :name)
Oh, espera, eso es incluso más feo. ;-)
Si quieres entrar en monkeypatching puedes hacer algo como esto
class NilClass def [](anything) nil end end
Entonces una llamada a params[:company][:owner][:name]
arrojará nulo si en algún punto uno de los hashes anidados es nulo.
EDITAR: Si quieres una ruta más segura que también proporcione código limpio, podrías hacer algo como
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
El código se vería así: params.chain(:company, :owner, :name)
Yo escribiría esto como:
name = params[:company] && params[:company][:owner] && params[:company][:owner][:name]
No es tan limpio como el ? operador en Io , pero Ruby no tiene eso. La respuesta de @ThiagoSilveira también es buena, aunque será más lenta.
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