如何检查一个键是否存在于Perl深层哈希中?

30
如果我理解正确, 调用if (exists $ref->{A}->{B}->{$key}) { ... }即使在if之前它们不存在,也会创建$ref->{A}$ref->{A}->{B}
这似乎是高度不希望的。那么我应该如何检查是否存在“深层”哈希键?

3
我很惊讶这个问题没有在perlfaq中,考虑到这比那里已经有的大部分问题更加常见。给我几分钟,我会解决这个问题 :) - brian d foy
10
看,这就是在perlfaq4中的如何检查多级哈希中是否存在键?。这基本上是这个线程的总结。感谢StackOverflow :) - brian d foy
链接中的部分已被修剪或更改-现在的链接是 https://perldoc.perl.org/perlfaq4#How-can-I-check-if-a-key-exists-in-a-multilevel-hash?. - Richlv
5个回答

40

最好使用像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
你可以使用autovivification 声明来取消自动创建引用。
use strict;
use warnings;
no autovivification;

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

print join ', ', keys %foo;

它也是词法作用域的,这意味着它只会在您指定的作用域内停用它。


1
“Can't locate autovivification.pm in @INC”?! - David B
5
@David:自动赋值(Autovivification)一直存在。这个模块只是让你对它有更好的控制。 - phaylon
7
@David:您拥有自动引用功能。只有在安装自动引用功能之前,才会出现“无自动引用功能”的情况。 - runrig

10

在查看顶层之前,请检查每个级别是否存在。

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

如果你觉得那很烦人,你可以随时在CPAN上查找。例如,有Hash::NoVivify


5
@David 没有区别。只有第一个箭头是有作用的,花括号 {} 和方括号 [] 之间连续的箭头是不必要的,通常最好将它们省略。 - hobbs
5
垃圾;使用 &&;仅将 and 用于流程控制。 - ysth
4
@ysth 坏味道回到你身上。我更喜欢低优先级运算符。 - Chas. Owens
2
如果你真的关心优先级,就把东西用括号括起来。 - CanSpice
5
在逻辑比较中,使用&&代替and。在流程控制中,使用and代替&& - vol7ron
显示剩余4条评论

6

看看Data::Diver。例如:

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));

1

虽然不太美观,但如果$ref是一个复杂的表达式,你不想在重复的exists测试中使用:

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

3
那是一种可憎的东西。我只是试图看它而已就眼花缭乱了。你还要创建高达 n-1(其中 n 是哈希表级数)个匿名哈希引用,仅为了避免在目标哈希表中进行自动vivification(你在匿名哈希引用中实现自动vivify)。我想知道与正常代码中多次调用“exist”相比,性能如何。 - Chas. Owens
@Chas. Owens:性能可能会更差,甚至可能会慢很多倍,但这并不重要,因为它只需要极少量的时间。 - ysth
1
如果所有的键都存在,实际上会快大约三倍。在那之后,理智版本开始获胜,但它们都可以执行超过一百万次每秒,所以无论哪种方式都没有真正的好处。这是我使用的基准测试 - Chas. Owens
@Chas. Owens:这就是我说的 :) 但是您的正常代码并不能保护 $ref 本身免受自动生成的影响。 - ysth
1
这种方法的问题在于,您必须为每组键和深度重新编写相同的代码。这里没有可重用性。 - brian d foy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接