2010-09-13 89 views
24

如果我understand correctly,调用if (exists $ref->{A}->{B}->{$key}) { ... }将弹出$ref->{A}$ref->{A}->{B}存在,即使它们在if之前不存在!如何检查密钥是否存在于深度Perl哈希中?

这似乎是非常不想要的。那么我应该如何检查一个“深”散列键是否存在?

+3

我很惊讶,这不是在perlfaq,考虑到它更FA比大多数的QS已经在那里。给我几分钟,我会修复:) – 2010-09-14 05:00:34

+9

哦,看,它在perlfaq4:[我如何检查密钥是否存在于多级哈希?](http://faq.perl.org/ perlfaq4.html#How_can_I_check_if_a)。它基本上是这个线程的总结。谢谢StackOverflow :) – 2010-09-17 15:57:17

回答

35

这是更好的使用有点像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。也许你想检查所选路径的值是否为真,或者是一个字符串,或另一个散列引用,或其他。只要您确认路径存在,就提供正确的检查。在这个例子中,我传递了一个子程序引用,它将检查我离开的值。我可以检查东西我喜欢:

#!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 
+0

+1感谢您的精心设计! – 2010-09-14 10:20:49

8

检查每个级别的exist ence在查看最高级别之前。

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

如果您发现讨厌你总是可以看看上CPAN。例如,有Hash::NoVivify

+1

有点肮脏,不是吗? – 2010-09-13 11:56:50

+0

还有,'$ ref - > {A} {B} {C}'和'$ ref - > {A} - > {B} - > {C}'有区别吗? – 2010-09-13 12:04:17

+4

@大卫不,没有区别。唯一的箭头是第一个。连续的“{}”和“[]”之间的箭头是不必要的,通常看起来会更好。 – hobbs 2010-09-13 12:26:20

13

您可以使用autovivification编译停用自动创建引用:

use strict; 
use warnings; 
no autovivification; 

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

print join ', ', keys %foo; 

这也是词汇,这意味着它只会关闭它,你在其指定的范围内

+0

'无法在@ INC'中找到autovivification.pm ?!! – 2010-09-13 12:31:15

+3

该错误意味着您需要从CPAN下载并安装'autovivification'编译指示,就像其他任何模块一样。 – toolic 2010-09-13 12:46:23

+0

所以我有autovivification工作没有autovivification? – 2010-09-13 13:07:40

0

很丑陋,但如果$ ref是一个复杂的表达式,您不想在重复存在的测试中使用:

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

这是一种憎恶。我只是试着去看看它。您还正在为避免目标散列中的自动版本(而不是自动匿名散列参考)创建高达'n - 1'(其中'n'是散列中的层数)匿名散列函数。我想知道,与多重调用“存在”的合理代码相比,性能如何。 – 2010-09-13 13:46:39

+0

@Chas。欧文斯:表现可能更糟,也许更糟糕,考虑到它只需要微不足道的时间,这根本不重要。 – ysth 2010-09-13 13:51:29

+1

在这种情况下,所有的钥匙都存在三次左右的情况实际上会更好。在这之后,理智的版本开始获胜,但它们都可以每秒执行一百万次以上,所以这两种方式都没有真正的好处。这是我使用的[benchmark](http://codepad.org/tXIMrpVW)。 – 2010-09-13 14:07:49

5

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