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

Si Je comprendre correctement, appelant if (exists $ref->{A}->{B}->{$key}) { ... } va du printemps à l'existence $ref->{A} et $ref->{A}->{B} même si ils n'existaient pas avant l' if!

Cela semble hautement indésirables. Alors comment puis-je vérifier si une clé de hachage "profonde" existe?

25
demandé sur brian d foy 2010-09-13 15:45:18

5 réponses

C'est beaucoup mieux d'utiliser quelque chose comme autovivification module pour désactiver cette fonctionnalité, ou d'utiliser Données::Plongeur. Cependant, c'est l'une des tâches simples que je m'attendrais à ce qu'un programmeur sache faire par lui-même. Même si vous n'utilisez pas cette technique ici, vous devriez le savoir pour d'autres problèmes. C'est essentiellement ce que Data::Diver le fait une fois que vous enlevez son interface.

c'est facile une fois que vous avez l'astuce de parcourir une donnée la structure (si vous ne souhaitez pas utiliser un module qui le fait pour vous). Dans mon exemple, j'ai créer un check_hash sous-programme qui prend une référence de hachage et une référence de tableau de clés à vérifier. Il vérifie un niveau à la fois. Si la clé n'est pas là, elle ne retourne rien. Si la clé est là, il taille le hachage à juste cette partie du chemin et tente à nouveau avec la touche suivante. Le truc, c'est que $hash est toujours de la partie suivante de l'arbre pour vérifier. J'ai mis le exists dans un eval au cas où le niveau suivant n'est pas une référence hash. L'astuce est de ne pas échouer si la valeur de hachage à la fin du chemin est une sorte de fausse valeur. 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 pas avoir peur de tout le code dans le prochain morceau. L'important, c'est juste le check_hash sous-routine. Tout le reste est de test et de 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 dump 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 un autre chèque au lieu de 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 quoi que ce soit. C'est juste une question de fournir le bon contrôle une fois que vous avez vérifié que le chemin existe. Dans cet exemple, je passe une référence de sous-programme qui vérifiera la valeur que j'ai laissée. Je peux vérifier pour tout ce que j'aime bien:

#!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
36
répondu brian d foy 2015-08-27 13:40:48

Vous pouvez utiliser le autovivification pragma 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 qu'à l'intérieur de la portée spécifiée.

14
répondu phaylon 2010-09-13 12:21:26

Vérifiez chaque niveau pour exist nce avant de regarder le niveau supérieur.

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

Si vous trouvez ça ennuyeux, vous pouvez toujours chercher sur CPAN. Par exemple, il y a Hash::NoVivify.

8
répondu Chas. Owens 2010-09-13 12:03:49

regardez Données:: Diver. E. g.:

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));
5
répondu runrig 2010-09-13 15:18:52

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

if ( exists ${ ${ ${ $ref || {} }{A} || {} }{B} || {} }{key} ) {
0
répondu ysth 2010-09-13 13:36:18