Как я могу проверить, существует ли ключ в глубоком хэше Perl? - PullRequest
28 голосов
/ 13 сентября 2010

Если я правильно понимаю , вызов if (exists $ref->{A}->{B}->{$key}) { ... } возникнет $ref->{A} и $ref->{A}->{B}, даже если они не существовали до if!

Это кажется крайне нежелательным. Так как мне проверить, существует ли «глубокий» хеш-ключ?

Ответы [ 5 ]

38 голосов
/ 14 сентября 2010

Гораздо лучше использовать что-то вроде модуля autovivification , чтобы отключить эту функцию, или использовать Data :: Diver .Тем не менее, это одна из простых задач, которые программист должен знать, как делать самостоятельно.Даже если вы не используете эту технику здесь, вы должны знать это для других проблем.По сути, это то, что делает Data::Diver, как только вы удаляете его интерфейс.

Это легко, если вы освоите структуру данных (если вы не хотите использовать модуль, который делает это длявы).В моем примере я создаю подпрограмму check_hash, которая принимает ссылку на хэш и ссылку на массив ключей для проверки.Он проверяет один уровень за раз.Если ключа нет, он ничего не возвращает.Если ключ есть, он сокращает хэш только до этой части пути и повторяет попытку со следующим ключом.Хитрость в том, что $hash это всегда следующая часть дерева для проверки.Я поставил exists в eval на случай, если следующий уровень не является хеш-ссылкой.Хитрость заключается не в том, чтобы потерпеть неудачу, если хеш-значение в конце пути является своего рода ложным значением.Вот важная часть задачи:

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

   return unless @$keys;

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

   return 1;
   }

Не пугайтесь всего кода в следующем бите.Важной частью является только подпрограмма check_hash.Все остальное - тестирование и демонстрация:

#!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';
   }

Вот вывод (минус дамп данных):

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

Теперь вы можете захотеть сделать другую проверку вместо exists,Может быть, вы хотите проверить, что значение в выбранном пути - true, или строка, или другая ссылка на хеш, или что-то еще.Это просто вопрос правильной проверки, как только вы убедились, что путь существует.В этом примере я передаю ссылку на подпрограмму, которая проверит значение, с которым я остановился.Я могу проверить все, что мне нравится:

#!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';
    }

И его вывод:

     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
15 голосов
/ 13 сентября 2010

Вы можете использовать прагму autovivification , чтобы отключить автоматическое создание ссылок:

use strict;
use warnings;
no autovivification;

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

print join ', ', keys %foo;

Это также лексическое значение, означающее, что оно будет деактивировано только в той области, в которой вы его указали.

9 голосов
/ 13 сентября 2010

Проверьте каждый уровень на exist, прежде чем смотреть на верхний уровень.

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

Если вы находите это раздражающим, вы всегда можете посмотреть на CPAN . Например, есть Hash::NoVivify.

6 голосов
/ 13 сентября 2010

Взгляните на Data :: 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));
0 голосов
/ 13 сентября 2010

Довольно уродливо, но если $ ref является сложным выражением, которое вы не хотите использовать в повторяющихся существующих тестах:

if ( exists ${ ${ ${ $ref || {} }{A} || {} }{B} || {} }{key} ) {
...