迭代 Perl 散列键的最安全方法是什么?

如果我有一个包含大量(键,值)对的 Perl 散列,那么遍历所有键的首选方法是什么?我听说使用 each在某些方面可能会有意想不到的副作用。那么,这是真的吗? 下面两种方法中的一种是最好的吗? 还是有更好的方法?

# Method 1
while (my ($key, $value) = each(%hash)) {
# Something
}


# Method 2
foreach my $key (keys(%hash)) {
# Something
}
130512 次浏览

我可能会被这个咬,但我认为这是个人偏好。我在文档中找不到任何关于每个()与 key ()或 value ()不同的引用(除了显而易见的“它们返回不同的东西”答案之外)。事实上,文档声明使用相同的迭代器,并且它们都返回实际的列表值,而不是它们的副本,并且在使用任何调用迭代散列时修改散列是不好的。

尽管如此,我几乎总是使用 key () ,因为对我来说,通过散列本身访问键的值通常更需要自我记录。当值是对大型结构的引用并且散列的键已经存储在结构中时,我偶尔会使用 value () ,此时键是多余的,我不需要它。我认为在10年的 Perl 编程中,我每次()都用了2次,这两次可能都是错误的选择

我也总是使用方法2。使用这两种方法的唯一好处是,如果只是读取(而不是重新分配)哈希条目的值,那么就不需要不断地取消对哈希的引用。

经验法则是使用最适合您需要的函数。

如果您只需要键,并且不打算使用 中的任何值,那么使用 key () :

foreach my $key (keys %hash) { ... }

如果您只想要值,那么使用 value () :

foreach my $val (values %hash) { ... }

If you need the keys 还有 the values, use each():

keys %hash; # reset the internal iterator so a prior each() doesn't affect the loop
while(my($k, $v) = each %hash) { ... }

如果计划以任何方式更改散列的键,以便在迭代期间删除当前键,则不能使用 each ()。例如,使用 key ()创建一组具有双倍值的新大写键的代码可以很好地工作:

%h = (a => 1, b => 2);


foreach my $k (keys %h)
{
$h{uc $k} = $h{$k} * 2;
}

产生预期的散列结果:

(a => 1, A => 2, b => 2, B => 4)

但是使用 each ()做同样的事情:

%h = (a => 1, b => 2);


keys %h;
while(my($k, $v) = each %h)
{
$h{uc $k} = $h{$k} * 2; # BAD IDEA!
}

produces incorrect results in hard-to-predict ways. For example:

(a => 1, A => 2, b => 2, B => 8)

然而,这是安全的:

keys %h;
while(my($k, $v) = each %h)
{
if(...)
{
delete $h{$k}; # This is safe
}
}

所有这些都在 perl 文档中进行了描述:

% perldoc -f keys
% perldoc -f each

我通常使用 keys,我想不起来上一次使用或阅读使用 each是什么时候了。

不要忘记 map,这取决于您在循环中所做的事情!

map { print "$_ => $hash{$_}\n" } keys %hash;

使用 each 语法将防止同时生成整个密钥集。如果您正在使用一个绑定到数百万行的数据库的哈希表,那么这一点可能很重要。您不希望同时生成整个键列表并耗尽物理内存。在这种情况下,每个键都充当迭代器,而键实际上在循环开始之前生成整个数组。

因此,“ each”唯一真正有用的地方是当散列非常大时(与可用内存相比)。只有当散列本身不存在于内存中时才可能发生这种情况,除非您正在编写手持数据收集设备或内存较小的设备。

If memory is not an issue, usually the map or keys paradigm is the more prevelant and easier to read paradigm.

关于这个主题的一些杂乱的想法:

  1. 散列迭代器本身没有什么不安全的。在迭代散列时修改它的键是不安全的。(修改这些值是绝对安全的。)我能想到的唯一潜在的副作用是 values返回别名,这意味着修改别名将修改散列的内容。这是设计好的,但在某些情况下可能不是您想要的。
  2. John 的 接受的答案很好,但有一个例外: 文档很清楚,在遍历散列时添加键是不安全的。它可能对某些数据集有效,但对其他数据集则会失败,这取决于散列顺序。
  3. 如前所述,删除 each返回的最后一个密钥是安全的。对于 keys,这是 没有,因为 each是迭代器,而 keys返回一个列表。

在使用 each时,您应该注意的一件事是 将“状态”添加到散列的副作用(散列必须记住 下一个键是什么), which iterate over the whole hash in one go, this is usually not a 然而,你会遇到很难找到的问题(我从 经验;) ,当使用 each与语句,如 lastreturn在您之前退出 while ... each循环 已经处理了所有的钥匙。

在这种情况下,散列将记住它已经返回的键,并且 when you use each on it the next time (maybe in a totaly unrelated piece of 代码) ,它将继续在这个位置。

Example:

my %hash = ( foo => 1, bar => 2, baz => 3, quux => 4 );


# find key 'baz'
while ( my ($k, $v) = each %hash ) {
print "found key $k\n";
last if $k eq 'baz'; # found it!
}


# later ...


print "the hash contains:\n";


# iterate over all keys:
while ( my ($k, $v) = each %hash ) {
print "$k => $v\n";
}

这张照片:

found key bar
found key baz
the hash contains:
quux => 4
foo => 1

钥匙“ bar”和“ baz”怎么了? 他们还在那儿,但是 第二个 each从第一个中断的地方开始,到散列结束时停止,所以我们在第二个循环中从未看到它们。

each可能给你带来问题的地方在于它是一个真正的、无作用域的迭代器:

while ( my ($key,$val) = each %a_hash ) {
print "$key => $val\n";
last if $val; #exits loop when $val is true
}


# but "each" hasn't reset!!
while ( my ($key,$val) = each %a_hash ) {
# continues where the last loop left off
print "$key => $val\n";
}

如果您需要确保 each获得所有键和值,那么您需要确保首先使用 keysvalues(因为它会重置迭代器)。看看 文件

我会说:

  1. 使用对大多数人来说最容易阅读/理解的任何东西(我认为通常是键)
  2. 在整个代码库中始终使用您决定的任何内容。

这带来了两大优势:

  1. It's easier to spot "common" code so you can re-factor into functions/methiods.
  2. 未来的开发人员更容易维护。

我不认为在每个上面使用键会更昂贵,所以在代码中不需要为同一件事情使用两个不同的结构。