【问题标题】:Unexpected behavior of each每个人的意外行为
【发布时间】:2011-07-12 23:45:40
【问题描述】:
%h = (a => 1, b => 2);

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

输出是:

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

而不是

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

为什么?

【问题讨论】:

  • 通常,“意外”的意思是“我没有阅读文档”。当某些事情没有按您期望的方式工作时,请阅读文档。你不必猜测。 :)

标签: perl


【解决方案1】:

来自perldoc -f each

如果您在迭代哈希时添加或删除它的元素, 条目可能会被跳过或重复——所以不要这样做。例外:它 删除each() 最近返回的项目总是安全的。

【讨论】:

    【解决方案2】:

    循环正在动态更改%h,因此它解释了b 值的两倍(首先是b,然后是B)。 each 的语义通过从散列中删除一对然后返回它来工作,但是您随后在循环中添加它,因此它可能会在以后得到处理。您应该首先获取键,然后循环获取值。例如:

    my @keys = keys %h;
    foreach (@keys)
    {
     $h{uc $_} = $h{$_} * 2;
     delete $h{$_};
    }
    

    作为查斯。上面的欧文斯指出,当each 删除元素时,您也必须删除它们。

    你可以做的另一件可爱的事情是使用 map 创建一个新的哈希:

    my %result  = map {uc $_ => $h{$_} * 2} (keys %h);
    

    然后使用哈希%result

    【讨论】:

    • 为什么a没有被解释两次?
    • 在第一种情况下,因为首先计算键。在第二种情况下,因为 map 正在创建一个 new 哈希。
    【解决方案3】:

    因为each 不允许您像for 循环那样修改项目。 each 只返回哈希的下一个键和值。当您说$h{uc $k} = $h{$k} * 2; 时,您正在哈希中创建新值。为了得到你想要的行为,我可能会说

    for my $k (keys %h) {
        $h{uc $k} = $h{$k};
        delete $h{$k};
    }
    

    如果哈希很大并且您担心将所有键存储在内存中(这是each 的主要用途),那么您最好说:

    my %new_hash;
    while (my ($k, $v) = each %h) {
        $new_hash{uc $k} = $v;
        delete $h{$k};
    }
    

    然后使用%new_hash 而不是%h

    至于为什么有些键会被多次处理,而有些则不会,首先我们必须看看the documentation for each

    如果在迭代哈希时添加或删除元素,条目可能会被跳过或重复——所以不要这样做。

    这很好,它告诉我们会发生什么,但不告诉我们为什么。要了解为什么我们必须创建一个正在发生的事情的模型。当您将值分配给散列时,键将通过hash function 转换为数字。然后这个数字被用来索引到一个数组中(在 C 级别,而不是 Perl 级别)。出于我们的目的,我们可以使用一个非常简单的模型:

    #!/usr/bin/perl
    
    use strict;
    use warnings;
    
    use Data::Dumper;
    
    my %hash_function = (
            a => 2,
            b => 1,
            A => 0,
            B => 3
    );
    
    my @hash_table;
    
    {
        my $position = 0;
        sub my_each {
            #return nothing if there is nothing
            return unless @hash_table;
    
            #get the key and value from the next positon in the
            #hash table, skipping empty positions
            until (defined $hash_table[$position]) {
                $position++;
                #return nothing if there is nothing left in the array
                return if $position > $#hash_table;
            }
            my ($k, $v) = %{$hash_table[$position]};
    
            #set up for the next call
            $position++;
    
            #if in list context, return both key an value
            #if in scalar context, return the key
            return wantarray ? ($k, $v) : $k;
        }
    }
    
    
    $hash_table[$hash_function{a}] = { a => 1 }; # $h{a} = 1;
    $hash_table[$hash_function{b}] = { b => 2 }; # $h{b} = 2;
    
    while (my ($k, $v) = my_each) {
        # $h{$k} = $v * 2;
        $hash_table[$hash_function{uc $k}] = { uc $k => $v * 2 };
    }
    
    print Dumper \@hash_table;
    

    对于这个例子,我们可以看到,当键 "A" 被添加到哈希表时,它被放在其他键之前,所以它不会被第二次处理,但是键 "B" 确实 放置在其他键之后,因此my_each 函数在第一次通过时会看到它(作为键"a" 之后的项目)。

    【讨论】:

    • 好像B被计算了两次,为什么?
    • 因为更改条目可能会更改它们的存储顺序,从而使“每个”迭代器正在使用的数据无效。
    【解决方案4】:

    这对我有用

    %h = (a => 1, b => 2);
    keys %h;
    for my $k (keys %h ) {
        $h{uc $k} = $h{$k} * 2;
    }
    while ( ($k,$v) = each %h ) {
        print "$k => $v\n";
    }
    

    输出:

    A => 2
    a => 1
    b => 2
    B => 4
    

    【讨论】:

    • 这是由于所使用的散列函数的性质。不同版本的 Perl 具有稍微不同的散列函数和/或种子(Perl 5.8.0 实际上保证每次运行程序时随机排序)。 Perl 保证只要您不添加或删除键,键的顺序将保持不变。这就是为什么each 的文档说“如果您在迭代时添加或删除哈希的元素,条目可能会被跳过或重复——所以不要这样做。”
    【解决方案5】:

    在你的循环中添加warn $k; 可能会让事情更清楚一些——我得到的结果和你一样,这是因为它最终使用的键是'a'、'b'然后是'B ',所以:

    #round 1 ($k='a'):
    $h{uc 'a'} = 1 * 2;
    # $h{A} = 2;
    
    #round 2: ($k='b'):
    $h{uc 'b'} = 2 * 2;
    # $h{B} = 4;
    
    #round 3: ($k='B'):
    $h{uc 'B'} = 4 * 2;
    # $h{B} = 8;
    

    为什么它使用键“B”而不是“A”运行循环?这是因为 each 调用在每次通过循环时都会运行(因此它正在使用新版本的哈希),但它会记住它正在使用的最后一个值,所以在这种情况下,当'A' 被添加到哈希中,它被分配在 'a' 之前的位置,所以它永远不会被看到。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-10-04
      • 2016-07-16
      • 2016-05-10
      • 2020-07-23
      • 2021-08-23
      • 2021-11-16
      • 2017-10-20
      • 2016-06-29
      相关资源
      最近更新 更多