30 votes

Comment puis-je vérifier si une clé existe dans un hachage Perl profond?

Si je comprends bien, appeler if (exists $ref->{A}->{B}->{$key}) { ... } fera apparaître $ref->{A} et $ref->{A}->{B} même s'ils n'existaient pas avant le if !

Cela semble très indésirable. Alors comment vérifier si une clé de hachage "profonde" existe ?

40voto

brian d foy Points 71781

C'est bien mieux d'utiliser quelque chose comme le module d'autovivification pour désactiver cette fonctionnalité, ou d'utiliser Data::Diver. Cependant, c'est l'une des tâches simples que j'attendrais d'un programmeur qu'il sache faire lui-même. Même si vous n'utilisez pas cette technique ici, vous devriez la connaître pour d'autres problèmes. C'est essentiellement ce que Data::Diver fait une fois que vous avez enlevé son interface.

C'est facile une fois que vous avez compris le truc pour parcourir une structure de données (si vous ne voulez pas utiliser un module qui le fait pour vous). Dans mon exemple, je crée une sous-routine check_hash qui prend une référence de hachage et une référence de tableau des clés à vérifier. Elle vérifie un niveau à la fois. Si la clé n'est pas là, rien n'est renvoyé. Si la clé est présente, le hachage est élagué pour juste cette partie du chemin et on essaie à nouveau avec la prochaine clé. Le truc est que $hash est toujours la prochaine partie de l'arborescence à vérifier. Je mets le exists dans un eval au cas où le prochain niveau n'est pas une référence de hachage. Le truc est de ne pas échouer si la valeur du hachage à la fin du chemin est une sorte de valeur fausse. Voici la partie importante de la tâche :

sub check_hash {
   my( $hash, $keys ) = @_;

   return unless @$keys;

   foreach my $key ( @$keys ) {
       return unless eval { exists $hash->{$key} };
       $hash = $hash->{$key};
       }

   return 1;
   }

Ne soyez pas effrayé par tout le code dans la prochaine partie. La partie importante est juste la sous-routine check_hash. Tout le reste est du test et de la démonstration :

#!perl
use strict;
use warnings;
use 5.010;

sub check_hash {
   my( $hash, $keys ) = @_;

   return unless @$keys;

   foreach my $key ( @$keys ) {
       return unless eval { exists $hash->{$key} };
       $hash = $hash->{$key};
       }

   return 1;
   }

my %hash = (
   a => {
       b => {
           c => {
               d => {
                   e => {
                       f => 'foo!',
                       },
                   f => 'foo!',
                   },
               },
           f => 'foo!',
           g => 'goo!',
           h => 0,
           },
       f => [ qw( foo goo moo ) ],
       g => undef,
       },
   f => sub { 'foo!' },
   );

my @paths = (
   [ qw( a b c d     ) ], # true
   [ qw( a b c d e f ) ], # true
   [ qw( b c d )       ], # false
   [ qw( f b c )       ], # false
   [ qw( a f )         ], # true
   [ qw( a f g )       ], # false
   [ qw( a g )         ], # true
   [ qw( a b h )       ], # false
   [ qw( a )           ], # true
   [ qw( )             ], # false
   );

say Dumper( \%hash ); use Data::Dumper; # just to remember the structure    
foreach my $path ( @paths ) {
   printf "%-12s --> %s\n", 
       join( ".", @$path ),
       check_hash( \%hash, $path ) ? 'true' : 'false';
   }

Voici la sortie (moins le déversement de données) :

a.b.c.d      --> true
a.b.c.d.e.f  --> true
b.c.d        --> false
f.b.c        --> false
a.f          --> true
a.f.g        --> false
a.g          --> true
a.b.h        --> true
a            --> true
             --> false

Maintenant, vous voudrez peut-être avoir une autre vérification sauf que exists. Peut-être que vous voulez vérifier que la valeur au chemin choisi est vraie, ou une chaîne, ou une autre référence de hachage, ou autre chose. C'est juste une question de fournir la bonne vérification une fois que vous avez vérifié que le chemin existe. Dans cet exemple, je passe une référence de sous-routine qui vérifiera la valeur avec laquelle j'ai laissé. Je peux vérifier n'importe quoi que je veux :

#!perl
use strict;
use warnings;
use 5.010;

sub check_hash {
    my( $hash, $sub, $keys ) = @_;

    return unless @$keys;

    foreach my $key ( @$keys ) {
        return unless eval { exists $hash->{$key} };
        $hash = $hash->{$key};
        }

    return $sub->( $hash );
    }

my %hash = (
    a => {
        b => {
            c => {
                d => {
                    e => {
                        f => 'foo!',
                        },
                    f => 'foo!',
                    },
                },
            f => 'foo!',
            g => 'goo!',
            h => 0,
            },
        f => [ qw( foo goo moo ) ],
        g => undef,
        },
    f => sub { 'foo!' },
    );

my %subs = (
    hash_ref  => sub {   ref $_[0] eq   ref {}  },
    array_ref => sub {   ref $_[0] eq   ref []  },
    true      => sub { ! ref $_[0] &&   $_[0]   },
    false     => sub { ! ref $_[0] && ! $_[0]   },
    exist     => sub { 1 },
    foo       => sub { $_[0] eq 'foo!' },
    'undef'   => sub { ! defined $_[0] },
    );

my @paths = (
    [ exist     => qw( a b c d     ) ], # true
    [ hash_ref  => qw( a b c d     ) ], # true
    [ foo       => qw( a b c d     ) ], # false
    [ foo       => qw( a b c d e f ) ], # true
    [ exist     => qw( b c d )       ], # false
    [ exist     => qw( f b c )       ], # false
    [ array_ref => qw( a f )         ], # true
    [ exist     => qw( a f g )       ], # false
    [ 'undef'   => qw( a g )         ], # true
    [ exist     => qw( a b h )       ], # false
    [ hash_ref  => qw( a )           ], # true
    [ exist     => qw( )             ], # false
    );

say Dumper( \%hash ); use Data::Dumper; # just to remember the structure    
foreach my $path ( @paths ) {
    my $sub_name = shift @$path;
    my $sub = $subs{$sub_name};
    printf "%10s --> %-12s --> %s\n", 
        $sub_name, 
        join( ".", @$path ),
        check_hash( \%hash, $sub, $path ) ? 'true' : 'false';
    }

Et sa sortie :

     exist --> a.b.c.d      --> true
  hash_ref --> a.b.c.d      --> true
       foo --> a.b.c.d      --> false
       foo --> a.b.c.d.e.f  --> true
     exist --> b.c.d        --> false
     exist --> f.b.c        --> false
 array_ref --> a.f          --> true
     exist --> a.f.g        --> false
     undef --> a.g          --> true
     exist --> a.b.h        --> true
  hash_ref --> a            --> true
     exist -->              --> false

15voto

phaylon Points 1474

Vous pourriez utiliser la pragma autovivification pour désactiver la création automatique de références :

use strict;
use warnings;
no autovivification;

my %foo;
print "yes\n" if exists $foo{bar}{baz}{quux};

print join ', ', keys %foo;

C'est aussi lexical, ce qui signifie qu'il ne le désactivera que dans la portée que vous spécifiez.

10voto

Chas. Owens Points 40887

Vérifiez chaque niveau pour l'existence avant de regarder le niveau supérieur.

if (exists $ref->{A} and exists $ref->{A}{B} and exists $ref->{A}{B}{$key}) {
}

Si cela vous dérange, vous pouvez toujours consulter CPAN. Par exemple, il y a Hash::NoVivify.

6voto

runrig Points 5422

Jetez un œil à Data::Diver. Par exemple :

use Data::Diver qw(Dive);

my $ref = { A => { foo => "bar" } };
my $value1 = Dive($ref, qw(A B), $key);
my $value2 = Dive($ref, qw(A foo));

1voto

ysth Points 54757

Joli laid, mais si $ref est une expression compliquée que vous ne voulez pas utiliser dans des tests d'existence répétés:

if ( existe ${ ${ ${ $ref || {} }{A} || {} }{B} || {} }{clé} ) {

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X