Je travaille un petit utilitaire écrit en Ruby qui utilise largement les hachages imbriqués. Actuellement, je vérifie l'accès aux éléments de hachage imbriqués comme suit:
structure = { :a => { :b => 'foo' }}
# I want structure[:a][:b]
value = nil
if structure.has_key?(:a) && structure[:a].has_key?(:b) then
value = structure[:a][:b]
end
Y a-t-il une meilleure manière de faire cela? Je voudrais pouvoir dire:
value = structure[:a][:b]
Et obtenez nil
si: a n'est pas une clé dans structure
, etc.
La façon dont je le fais habituellement ces jours-ci est la suivante:
h = Hash.new { |h,k| h[k] = {} }
Cela vous donnera un hachage qui crée un nouveau hachage comme entrée pour une clé manquante, mais renvoie zéro pour le deuxième niveau de clé:
h['foo'] -> {}
h['foo']['bar'] -> nil
Vous pouvez l'imbriquer pour ajouter plusieurs couches qui peuvent être traitées de cette façon:
h = Hash.new { |h, k| h[k] = Hash.new { |hh, kk| hh[kk] = {} } }
h['bar'] -> {}
h['tar']['zar'] -> {}
h['scar']['far']['mar'] -> nil
Vous pouvez également chaîner indéfiniment en utilisant le default_proc
méthode:
h = Hash.new { |h, k| h[k] = Hash.new(&h.default_proc) }
h['bar'] -> {}
h['tar']['star']['par'] -> {}
Le code ci-dessus crée un hachage dont le proc par défaut crée un nouveau hachage avec le même proc par défaut. Ainsi, un hachage créé comme valeur par défaut lors de la recherche d'une clé non vue aura le même comportement par défaut.
EDIT: Plus de détails
Les hachages Ruby vous permettent de contrôler la façon dont les valeurs par défaut sont créées lorsqu'une recherche se produit pour une nouvelle clé. Lorsqu'il est spécifié, ce comportement est encapsulé sous la forme d'un objet Proc
et est accessible via default_proc
et default_proc=
méthodes. Le proc par défaut peut également être spécifié en passant un bloc à Hash.new
.
Décomposons un peu ce code. Ce n'est pas un Ruby idiomatique, mais il est plus facile de le décomposer en plusieurs lignes:
1. recursive_hash = Hash.new do |h, k|
2. h[k] = Hash.new(&h.default_proc)
3. end
La ligne 1 déclare une variable recursive_hash
pour être un nouveau Hash
et commence un bloc pour être recursive_hash
's default_proc
. Le bloc contient deux objets: h
, qui est l'instance Hash
sur laquelle la recherche de clé est effectuée, et k
, la clé recherchée.
La ligne 2 définit la valeur par défaut dans le hachage sur une nouvelle instance Hash
. Le comportement par défaut de ce hachage est fourni en passant un Proc
créé à partir du default_proc
du hachage dans lequel se produit la recherche; c'est-à-dire, le proc par défaut que le bloc lui-même définit.
Voici un exemple d'une session IRB:
irb(main):011:0> recursive_hash = Hash.new do |h,k|
irb(main):012:1* h[k] = Hash.new(&h.default_proc)
irb(main):013:1> end
=> {}
irb(main):014:0> recursive_hash[:foo]
=> {}
irb(main):015:0> recursive_hash
=> {:foo=>{}}
Lorsque le hachage à recursive_hash[:foo]
a été créé, son default_proc
a été fourni par recursive_hash
's default_proc
. Cela a deux effets:
recursive_hash[:foo]
est le même que recursive_hash
.recursive_hash[:foo]
's default_proc
sera identique à recursive_hash
.Donc, en continuant à la CISR, nous obtenons ce qui suit:
irb(main):016:0> recursive_hash[:foo][:bar]
=> {}
irb(main):017:0> recursive_hash
=> {:foo=>{:bar=>{}}}
irb(main):018:0> recursive_hash[:foo][:bar][:zap]
=> {}
irb(main):019:0> recursive_hash
=> {:foo=>{:bar=>{:zap=>{}}}}
Traditionnellement, vous deviez vraiment faire quelque chose comme ça:
structure[:a] && structure[:a][:b]
Cependant, Ruby 2.3 a ajouté une méthode Hash#Dig
qui rend cette façon plus gracieuse:
structure.Dig :a, :b # nil if it misses anywhere along the way
Il y a une gemme appelée Ruby_Dig
qui corrigera cela à votre place.
Ruby 2.3.0 a introduit ne nouvelle méthode appelée Dig
sur Hash
et Array
qui résout entièrement ce problème.
value = structure.Dig(:a, :b)
Il renvoie nil
si la clé est manquante à n'importe quel niveau.
Si vous utilisez une version de Ruby antérieure à 2.3, vous pouvez utiliser le Ruby_Dig
gem ou implémentez-le vous-même:
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
Je pense que l'une des solutions les plus lisibles utilise Hashie :
require 'hashie'
myhash = Hashie::Mash.new({foo: {bar: "blah" }})
myhash.foo.bar
=> "blah"
myhash.foo?
=> true
# use "underscore dot" for multi-level testing
myhash.foo_.bar?
=> true
myhash.foo_.huh_.what?
=> false
value = structure[:a][:b] rescue nil
Solution 1
Je l'ai suggéré dans ma question avant:
class NilClass; def to_hash; {} end end
Hash#to_hash
est déjà défini et renvoie self. Ensuite, vous pouvez faire:
value = structure[:a].to_hash[:b]
Le to_hash
garantit que vous obtenez un hachage vide lorsque la recherche de clé précédente échoue.
Solution2
Cette solution est similaire dans son esprit à mu est une réponse trop courte en ce qu'elle utilise une sous-classe, mais toujours quelque peu différente. Dans le cas où il n'y a pas de valeur pour une certaine clé, elle n'utilise pas de valeur par défaut, mais crée plutôt une valeur de hachage vide, de sorte qu'elle n'a pas le problème de confusion dans l'assignation que la réponse de DigitalRoss a, comme cela a été souligné par mu est trop court.
class NilFreeHash < Hash
def [] key; key?(key) ? super(key) : self[key] = NilFreeHash.new end
end
structure = NilFreeHash.new
structure[:a][:b] = 3
p strucrture[:a][:b] # => 3
Il s'écarte cependant de la spécification donnée dans la question. Lorsqu'une clé non définie est donnée, elle renverra une instruction de hachage vide de nil
.
p structure[:c] # => {}
Si vous créez une instance de ce NilFreeHash depuis le début et affectez les valeurs-clés, cela fonctionnera, mais si vous souhaitez convertir un hachage en une instance de cette classe, cela peut être un problème.
require 'xkeys'
structure = {}.extend XKeys::Hash
structure[:a, :b] # nil
structure[:a, :b, :else => 0] # 0 (contextual default)
structure[:a] # nil, even after above
structure[:a, :b] = 'foo'
structure[:a, :b] # foo
Cette fonction de patch de singe pour Hash devrait être la plus simple (au moins pour moi). Il ne modifie pas non plus la structure, c'est-à-dire le changement de nil
en {}
. Cela s'appliquerait également même si vous lisez un arbre à partir d'une source brute, par exemple JSON. Il n'a pas non plus besoin de produire des objets de hachage vides au fur et à mesure ou d'analyser une chaîne. rescue nil
était en fait une bonne solution facile pour moi car je suis assez courageux pour un risque aussi faible, mais je trouve que cela a essentiellement un inconvénient avec les performances.
class ::Hash
def recurse(*keys)
v = self[keys.shift]
while keys.length > 0
return nil if not v.is_a? Hash
v = v[keys.shift]
end
v
end
end
Exemple:
> structure = { :a => { :b => 'foo' }}
=> {:a=>{:b=>"foo"}}
> structure.recurse(:a, :b)
=> "foo"
> structure.recurse(:a, :x)
=> nil
Ce qui est également bien, c'est que vous pouvez jouer avec les tableaux enregistrés avec:
> keys = [:a, :b]
=> [:a, :b]
> structure.recurse(*keys)
=> "foo"
> structure.recurse(*keys, :x1, :x2)
=> nil
Vous pouvez simplement créer une sous-classe Hash avec une méthode variadique supplémentaire pour creuser tout le long avec des vérifications appropriées en cours de route. Quelque chose comme ça (avec un meilleur nom bien sûr):
class Thing < Hash
def find(*path)
path.inject(self) { |h, x| return nil if(!h.is_a?(Thing) || h[x].nil?); h[x] }
end
end
Ensuite, utilisez simplement Thing
s au lieu des hachages:
>> x = Thing.new
=> {}
>> x[:a] = Thing.new
=> {}
>> x[:a][:b] = 'k'
=> "k"
>> x.find(:a)
=> {:b=>"k"}
>> x.find(:a, :b)
=> "k"
>> x.find(:a, :b, :c)
=> nil
>> x.find(:a, :c, :d)
=> nil
J'essaye actuellement ceci:
# --------------------------------------------------------------------
# System so that we chain methods together without worrying about nil
# values (a la Objective-c).
# Example:
# params[:foo].try?[:bar]
#
class Object
# Returns self, unless NilClass (see below)
def try?
self
end
end
class NilClass
class MethodMissingSink
include Singleton
def method_missing(meth, *args, &block)
end
end
def try?
MethodMissingSink.instance
end
end
Je connais les arguments contre try
, mais c'est utile lorsque l'on regarde des choses, comme disons, params
.
Dans mon cas, j'avais besoin d'une matrice à deux dimensions où chaque cellule est une liste d'éléments.
J'ai trouvé cette technique qui semble fonctionner. Cela pourrait fonctionner pour le PO:
$all = Hash.new()
def $all.[](k)
v = fetch(k, nil)
return v if v
h = Hash.new()
def h.[](k2)
v = fetch(k2, nil)
return v if v
list = Array.new()
store(k2, list)
return list
end
store(k, h)
return h
end
$all['g1-a']['g2-a'] << '1'
$all['g1-a']['g2-a'] << '2'
$all['g1-a']['g2-a'] << '3'
$all['g1-a']['g2-b'] << '4'
$all['g1-b']['g2-a'] << '5'
$all['g1-b']['g2-c'] << '6'
$all.keys.each do |group1|
$all[group1].keys.each do |group2|
$all[group1][group2].each do |item|
puts "#{group1} #{group2} #{item}"
end
end
end
La sortie est:
$ Ruby -v && Ruby t.rb
Ruby 1.9.2p0 (2010-08-18 revision 29036) [x86_64-linux]
g1-a g2-a 1
g1-a g2-a 2
g1-a g2-a 3
g1-a g2-b 4
g1-b g2-a 5
g1-b g2-c 6
Vous pouvez utiliser la gemme andand , mais je m'en méfie de plus en plus:
>> structure = { :a => { :b => 'foo' }} #=> {:a=>{:b=>"foo"}}
>> require 'andand' #=> true
>> structure[:a].andand[:b] #=> "foo"
>> structure[:c].andand[:b] #=> nil
Il y a la façon mignonne mais mauvaise de le faire. Qui consiste à monkey-patch NilClass
pour ajouter un []
méthode qui renvoie nil
. Je dis que c'est la mauvaise approche car vous n'avez aucune idée de ce que d'autres logiciels peuvent avoir fait une version différente, ou quel changement de comportement dans une future version de Ruby peut être cassé par cela.
Une meilleure approche consiste à créer un nouvel objet qui fonctionne un peu comme nil
mais prend en charge ce comportement. Faites de ce nouvel objet le retour par défaut de vos hachages. Et puis ça marchera.
Alternativement, vous pouvez créer une simple fonction de "recherche imbriquée" à laquelle vous passez le hachage et les clés, qui parcourt les hachages dans l'ordre, éclatant quand il le peut.
Je préférerais personnellement l'une des deux dernières approches. Bien que je pense que ce serait mignon si le premier était intégré dans le langage Ruby. (Mais le patch de singe est une mauvaise idée. Ne faites pas ça. Surtout pour ne pas démontrer ce qu'est un hacker cool) tu es.)
Non pas que je le ferais, mais vous pouvez Monkeypatch dans NilClass#[]
:
> structure = { :a => { :b => 'foo' }}
#=> {:a=>{:b=>"foo"}}
> structure[:x][:y]
NoMethodError: undefined method `[]' for nil:NilClass
from (irb):2
from C:/Ruby/bin/irb:12:in `<main>'
> class NilClass; def [](*a); end; end
#=> nil
> structure[:x][:y]
#=> nil
> structure[:a][:y]
#=> nil
> structure[:a][:b]
#=> "foo"
Allez avec la réponse de @ DigitalRoss. Oui, c'est plus de frappe, mais c'est parce que c'est plus sûr.