开发者

Why doesn't Perl's for() go through all of the elements in my array?

Have a perl brain-teaser:

my @l = ('a', 'b', 'c');
for (@l) {
    my $n = 1;
    print shift @l while (@l and $n --> 0);
    print "\n";
}

What's it print? Should be a, b, and c, right? But oh wait actually there's a bug somewhere, it only prints a and b. Probably just some stupid off-by-one, should be easy to solve, right?

Ok so make a small code change to test things out and change @l to

my @l = ('a', 'b', 'c', 'd');

What's it print? Probably a, b, and c because of that stupid off by one, right? ...Wait a second, actually it still prints only a and b. Okay, so the bug is that it only prints the first two characters.

Change @l again to

my @l = ('a', 'b', 'c', 'd', 'e');

Uhm, now it prints a, b, and c. But not d or e. I开发者_StackOverflow社区n fact, every 2 letters we add from now on will make it print the next letter in the sequence. So if we add f it'll still just print a, b, and c, but if we add f and g it'll print a, b, c, and d.

This also happens with similar results for different values of $n.

So what's going on here?


Dave Webb beat me to the problem, but here's a quote from perldoc perlsyn saying not to do it:

If any part of LIST is an array, foreach will get very confused if you add or remove elements within the loop body, for example with splice. So don't do that.

Note that, earlier in the text, the syntax of foreach was described as foreach LIST, which is the LIST they refer to in the documentation. Note also that foreach and for are equivalent.


What's going on is that you're using for and shift at the same time. So you're looping through the list whilst modifying it, not a good idea.


I think this is somebody's gadget code. It doesn't look like the way you would want to write anything. But what it might illustrate best is that (in at least some versions) Perl is really running a more basic for loop, where:

for ( @l ) { 
    #...
}

Is replaced by:

for ( my $i = 0; $i < @l; $i++ ) { 
    local $_ = $l[$i];
    #...
}    

Thus, because @l is ( 'c' ) when we've gone through twice, our trips through is already greater than scalar( @l ), so we're out. I've tested it out in a number of cases, and they seem to be equivalent.

Below is the code I wrote to test cases. From it we can see that because of the shift, as soon as we're halfway through, the loop will exit.

use strict;
use warnings;
use English qw<$LIST_SEPARATOR>;
use Test::More 'no_plan';

sub test_loops_without_shifts { 
    my @l = @_;
    my @tests;
    for ( @l ) { 
        push @tests, $_;
    }
    my @l2 = @_;
    my $n  = @tests;
    my $i = 0;
    for ( $i = 0; $i < @l2; $i++ ) { 
        local $_ = $l2[$i];
        my $x = shift @tests;
        my $g = $_;
        is( $g, $x, "expected: $x, got: $g" );
    }
    is( $n, $i );
    is_deeply( \@l, \@l2, do { local $LIST_SEPARATOR = .', '; "leftover: ( @l ) = ( @l2 )" } );
    return $i;
}

sub test_loops { 
    my @l = @_;
    my @tests;
    for ( @l ) { 
        push @tests, shift @l;
    }
    my @l2 = @_;
    my $n  = @tests;
    my $i = 0;
    for ( $i = 0; $i < @l2; $i++ ) { 
        local $_ = $l2[$i];
        my $x = shift @tests;
        my $g = shift @l2;
        is( $g, $x, "expected: $x, got: $g" );
    }
    is( $n, $i );
    is_deeply( \@l, \@l2, do { local $LIST_SEPARATOR = ', 'c; "leftover: ( @l ) = ( @l2 )" } );
    return $i;
}

is( test_loops( 'a'..'c' ), 2 );
is( test_loops( 'a'..'d' ), 2 );
is( test_loops( 'a'..'e' ), 3 );
is( test_loops( 'a'..'f' ), 3 );
is( test_loops( 'a'..'g' ), 4 );
is( test_loops_without_shifts( 'a'..'c' ), 3 );
is( test_loops_without_shifts( 'a'..'d' ), 4 );
is( test_loops_without_shifts( 'a'..'e' ), 5 );
is( test_loops_without_shifts( 'a'..'f' ), 6 );
is( test_loops_without_shifts( 'a'..'g' ), 7 );
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜