Perl的each函数值得使用吗?

12

perldoc -f each中我们可以得知:

每个哈希表都有一个单独的迭代器,由程序中所有each, keysvalues函数共享; 它可以通过从哈希表中读取所有元素或计算keys HASHvalues HASH来重置。

当您离开包含each()的作用域时,迭代器不会被重置,这可能会导致错误:

my %h = map { $_, 1 } qw(1 2 3);
while (my $k = each %h) { print "1: $k\n"; last }
while (my $k = each %h) { print "2: $k\n"       }

输出:

1: 1
2: 3
2: 2

有哪些常见的解决方法?总体来说,使用each值得吗?


5
我想常见的解决方法包括评估 keys HASHvalues HASH - Daniel LeCheminant
8个回答

10

只要你意识到这一点,我认为使用它是值得的。在需要迭代键和值时,它是最理想的选择:

while (my ($k,$v) = each %h) {
    say "$k = $v";
}

在您的示例中,您可以通过添加keys %h;来重置迭代器,如下所示:

my %h = map { $_ => 1 } qw/1 2 3/;
while (my $k = each %h) { print "1: $k\n"; last }
keys %h;  # reset %h
while (my $k = each %h) { print "2: $k\n" }
从Perl 5.12开始,each 也可以在数组上进行迭代。

8

我发现each对于像这样的习语非常方便:

my $hashref = some_really_complicated_method_that_builds_a_large_and_deep_structure();
while (my ($key, $value) = each %$hashref)
{
    # code that does stuff with both $key and $value
}

将该代码与以下代码进行对比:

my $hashref = ...same call as above
foreach my $key (keys %$hashref)
{
    my $value = $hashref->{$key};
    # more code here...
}

在第一种情况下,循环体内可以立即访问$key$value。在第二种情况下,需要先获取$value。此外,$hashref的键列表可能非常大,占用内存。这有时会成为问题。使用each不会产生这样的开销。
然而,each的缺点并不是立即显现的:如果从循环中提前退出,则哈希表的迭代器不会重置。此外(我认为这个更严重,甚至更不明显):您不能在此循环内调用keys()values()或另一个each()。这样做会重置迭代器,您将失去在while循环中的位置。while循环将永远继续,这肯定是一个严重的错误。

8
each太危险了,不建议使用,许多样式指南完全禁止使用它。危险在于,如果在哈希结构的循环中终止了each,下一个循环将从上一次结束的地方开始。这可能会导致非常难以重现的错误;程序的某个部分的行为将取决于程序中完全不相关的另一个部分。您可能正确使用each,但是每个使用您的哈希(或哈希引用)的模块呢? keysvalues始终安全,因此只需使用它们。无论如何,keys使遍历哈希更加确定性,这几乎总是更有用的。(for my $key (sort keys %hash) { ... }

你经常使用全局哈希表,对吗? - ysth
3
无论是全局的还是私有的属性,都有可能受到这个问题的影响。任何返回哈希引用的内容都会受到影响。 - jrockway

7

每一个都值得使用,如果您想循环遍历所有绑定哈希表中的内容,那么它几乎是必需的,但是内存不足时也可以使用。

在开始循环之前,使用void-context keys()(或values,但保持一致性更好)是唯一需要的“解决方法”;您是否有其他解决方法的原因?


1
非常好的观点!这是我能想到的使用each的最佳(唯一?)理由。 - daotoad

2

each 内置一个隐藏的全局变量,可能会对您造成伤害。除非您需要此行为,否则最好只使用 keys

考虑以下示例,我们想要分组 k/v 对(是的,我知道 printf 可以更好地完成这项工作):

#!perl

use strict;
use warnings;

use Test::More 'no_plan';

{   my %foo = map { ($_) x 2 } (1..15);

    is( one( \%foo ), one( \%foo ), 'Calling one twice works with 15 keys' );
    is( two( \%foo ), two( \%foo ), 'Calling two twice works with 15 keys' );
}

{   my %foo = map { ($_) x 2 } (1..105);

    is( one( \%foo ), one( \%foo ), 'Calling one twice works with 105 keys' );
    is( two( \%foo ), two( \%foo ), 'Calling two twice works with 105 keys' );
}


sub one {
    my $foo = shift;

    my $r = '';

    for( 1..9 ) {
        last unless my ($k, $v) = each %$foo;

        $r .= "  $_: $k -> $v\n";
    }
    for( 10..99 ) {
        last unless my ($k, $v) = each %$foo;

        $r .= " $_: $k -> $v\n";
    }

    return $r;
}

sub two {
    my $foo = shift;

    my $r = '';

    my @k = keys %$foo;

    for( 1..9 ) {
        last unless @k;
        my $k = shift @k;

        $r .= "  $_: $k -> $foo->{$k}\n";
    }
    for( 10..99 ) {
        last unless @k;
        my $k = shift @k;

        $r .= "  $_: $k -> $foo->{$k}\n";
    }

    return $r;
}

在一个真实应用中调试上述测试中显示的错误将是非常痛苦的。(为了更好的输出,使用Test::Differenceseq_or_diff而不是is。)

当然,可以通过在子程序的开头和结尾使用keys清除迭代器来修复one()函数。只要你记得,并且所有同事都记得,那么这样做是完全安全的。

我不知道你是怎么想的,但我仍然坚持使用keysvalues


2
使用 keys() 函数来重置迭代器。更多信息请参见 faq

1
最好按照它的名字each使用。如果你的意思是“给我第一个键值对”或“给我前两个键值对”等,那么使用它可能是错误的。只需记住,这个想法足够灵活,每次调用它时,您都会得到下一个键值对(或在标量上下文中的键)。

1

如果你正在迭代一个绑定哈希表,例如包含数百万个键的数据库,那么使用each()可能会更有效率;这样你就不必将所有键加载到内存中。


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