Perl foreach 循环

一个 foreach 循环 会为列表中的每个元素运行一段代码。这没什么大不了的,“perl foreach” 仍然是 Google 上关于这种语言的搜索中最受欢迎的之一。因此,我们认为看看20年来发生了什么。我在汤姆·克里斯蒂安森的 幻灯片 上进行了扩展,这是他更长的演示的一部分,然后在最后添加了一个新的但实验性的功能。如果你想了解更多,可以在 perlsyn 或我的书中找到更多内容 Learning Perl。
遍历列表
除非你另有说明,否则 foreach
将当前元素别名到主题变量 $_
。你可以在 foreach
后面的括号中直接指定列表,使用数组变量,或使用子程序调用的结果(以及其他获取列表的方式)
foreach ( 1, 3, 7 ) {
print "\$_ is $_";
}
my @numbers = ( 1, 3, 7 );
foreach ( @numbers ) {
print "\$_ is $_";
}
sub numbers{ return ( 1, 3, 7 ) }
foreach ( numbers() ) {
print "\$_ is $_";
}
sub numbers{ keys %some_hash }
foreach ( numbers() ) {
print "\$_ is $_";
}
有些人喜欢使用同义词 for
。有一个正确的 C 风格的 for 循环,在括号中有三个由分号分隔的部分。如果 Perl 没有看到两个分号,它将 for
仅视为 foreach
for ( my $i = 0; $i < 5; $i++ ) { # C style
print "\$i is $i";
}
for ( 0 .. 4 ) { # foreach synonym
print "\$_ is $_";
}
元素源陷阱
别名是临时的。在 foreach
之后,主题变量将返回其原始值
$_ = "Original value";
my @numbers = ( 1, 3, 7 );
print "\$_ before: $_\n";
foreach ( @numbers ) {
print "\$_ is $_\n";
$_ = $_ * 2;
}
print "\$_ after: $_\n";
输出显示 $_
似乎未受 foreach
的影响
$_ before: Original value
$_ is 1
$_ is 3
$_ is 7
$_ after: Original value
这是一个别名而不是副本,这是一种快捷方式,可以让你的程序更快一些,因为它不会移动数据。如果你更改主题,如果列表源是数组,则更改原始值(否则值是只读的,你会得到一个错误)
my @numbers = ( 1, 3, 7 );
print "Before: @numbers\n"; # Before: 1 3 7
foreach ( @numbers ) {
print "\$_ is $_\n";
$_ = $_ * 2;
}
print "After: @numbers\n"; # After: 2 6 14
不仅如此,如果你通过添加或删除元素来更改源,你可能会搞砸 foreach
。因为这个循环会无限期地处理相同的元素,因为每次通过块都会将数组元素向前移动一个位置;当迭代器移动到下一个位置时,它会找到它刚刚看到的同一个元素
my @numbers = ( 1, 3, 7 );
print "\$number before: $number\n";
foreach $number ( @numbers ) {
print "\$number is $number\n";
unshift @numbers, "Added later";
}
这个输出将会无限期地进行下去
$number is 1
$number is 1
$number is 1
$number is 1
为自己的主题变量命名
$_
通常很有用,因为它是几个 Perl 函数的默认变量,例如 chomp 或 split。你可以通过在 foreach
和括号之间指定一个标量变量来使用自己的名称。通常你不想将该变量用于其他用途,因此通常的风格是在 foreach
内声明它
foreach my $number ( 1, 3, 7 ) {
print "\$number is $number";
}
由于 Perl 将列表展平为一个大的列表,你可以在括号中使用多个列表源
my @numbers = ( 1, 3, 7 );
my @more_numbers = ( 5, 8, 13 );
foreach my $number ( @numbers, @more_numbers ) {
print "\$number is $number";
}
或者源类型的混合
my @numbers = ( 1, 3, 7 );
my @more_numbers = ( 5, 8, 13 );
foreach my $number ( @numbers, numbers(), keys %hash ) {
print "\$number is $number";
}
使用自己的命名主题变量与你在 $_
中看到的情况一样
my @numbers = ( 1, 3, 7 );
my $number = 'Original value';
say "Before: $number";
foreach $number ( @numbers ) {
say "\$number is $number";
}
say "After: $number";
输出显示了别名效果,并在 foreach
之后恢复了原始值
Before: Original value
$number is 1
$number is 3
$number is 7
After: Original value
控制
有三个关键字可以让你控制 foreach
(以及其他循环结构)的操作:last
、next
和 redo
。
last
停止当前迭代。它就像立即跳过块中的最后一个语句然后退出循环一样。它不会查看下一个项目。你通常与后缀条件一起使用它
foreach $number ( 0 .. 5 ) {
say "Starting $number";
last if $number > 3;
say "\$number is $number";
say "Ending $number";
}
say 'Past the loop';
你开始对元素 3
进行块,但在那里结束循环并继续程序
Starting 0
$number is 0
Ending 0
Starting 1
$number is 1
Ending 1
Starting 2
$number is 2
Ending 2
Starting 3
Past the loop
next
停止当前迭代并转到下一个迭代。这使得跳过你不想处理的元素变得容易
foreach my $number ( 0 .. 5 ) {
say "Starting $number";
next if $number % 2;
say "\$number is $number";
say "Ending $number";
}
输出显示你为每个元素运行块,但只有偶数才能通过 next
Starting 0
$number is 0
Ending 0
Starting 1
Starting 2
$number is 2
Ending 2
Starting 3
Starting 4
$number is 4
Ending 4
Starting 5
使用 redo
可以重新开始一个块的当前迭代。虽然它通常与不是遍历项目列表的循环结构一起使用,但它也可以与 foreach
一起使用。
以下是一个示例,其中你想要获取三行“好”的输入。你遍历你想要的行数,并每次读取标准输入。如果你得到一个空白行,你可以使用以下方法重新启动相同的循环:
my $lines_needed = 3;
my @lines;
foreach my $animal ( 1 .. $lines_needed ) {
chomp( my $line = <STDIN> );
redo if $line =~ /\A \s* \z/x; # skip "blank" lines
push @lines, $line;
}
say "Lines are:\n\t", join "\n\t", @lines;
输出显示循环有效地忽略了空白行,并回到循环的顶部。不过,它不会使用列表中的下一个项目。在尝试读取第二行时,如果它遇到空白行,它会再次尝试读取第二行。
Reading line 1
First line
Reading line 2
Reading line 2
Reading line 2
Second line
Reading line 3
Reading line 3
Reading line 3
Third line
Lines are:
First line
Second line
Third line
这并不太像Perl的风格,但这是一篇关于 foreach
的文章。更好的做法可能是使用 while
读取行,直到 @lines
足够大。
my $lines_needed = 3;
my @lines;
while( <STDIN> ) {
next if /\A \s* \z/x;
chomp;
push @lines, $_;
last if @lines == $lines_needed;
}
say "Lines are:\n\t", join "\n\t", @lines;
你可以用这些做更多的事情。它们与标签和嵌套循环一起工作。你可以在 perlsyn 或 Learning Perl 中了解更多。
常见的文件读取陷阱
由于 foreach
会遍历列表中的每个元素,因此当人们想要遍历文件中的每一行时,他们就会想到使用它
foreach my $line ( <STDIN> ) { ... }
这通常不是一个好主意。foreach
需要一次获取整个列表。这不像在其他语言中看到的那样是一个懒惰的结构。这意味着 foreach
在执行任何操作之前会读取所有标准输入。如果标准输入没有关闭,程序看起来就像挂起了。或者更糟,它会尝试从这个文件句柄中完全读取千兆字节的数据。内存很便宜,但并不便宜。
一个合适的替代方法是逐行读取和处理的 while
习语
while( <STDIN> ) { ... }
这实际上是一个在标量上下文中进行赋值的快捷方式。它只从文件句柄中读取一行
while( defined( $_ = <STDIN> ) ) { ... }
一个实验性的便利功能
Perl v5.22 添加了一个 实验性的 refaliasing
功能。将值分配给引用会使右侧的对象成为左侧对象的别名。以下是一个小演示,其中将匿名散列分配给命名散列变量的引用。现在 %h
是那个散列引用的另一个名称(别名)
use feature qw(refaliasing);
use Data::Dumper;
\my %h = { qw(a 1 b 2) };
say Dumper( \%h );
这在 foreach
中很有用,其中列表的元素是散列引用。首先,这是在没有这个功能的情况下如何做的方法。在块内部,你将 $hash
作为引用交互;你必须取消引用以获取值
my @mascots = (
{
type => 'camel',
name => 'Amelia',
},
{
type => 'butterfly',
name => 'Camelia',
},
{
type => 'go',
name => 'Go Gopher',
},
{
type => 'python',
name => 'Monty',
},
);
foreach my $hash ( @mascots ) {
say $hash->{'name'}
}
使用 v5.22 的 refaliasing
功能,你可以使用命名散列变量作为主题。在块内部,你将当前元素作为命名散列交互。没有取消引用的 ->
use v5.22;
use feature qw(refaliasing);
use Data::Dumper;
my @mascots = (
{
type => 'camel',
name => 'Amelia',
},
{
type => 'butterfly',
name => 'Camelia',
},
{
type => 'go',
name => 'Go Gopher',
},
{
type => 'python',
name => 'Monty',
},
);
foreach \my %hash ( @mascots ) {
say $hash{'name'}
}
两个程序中的输出是相同的
Amelia
Camelia
Go Gopher
Monty
Aliasing via reference is experimental at ...
这个实验性功能有一个警告(以及所有这样的功能)。该功能可能会改变,甚至消失,根据 Perl 的功能政策。如果你对此感到舒服,请禁用警告
no warnings qw(experimental::refaliasing);
结论
foreach
是一种方便的方式,可以逐个遍历列表。当已经完全构建了列表(而不是处理文件句柄)时使用它。定义自己的主题变量以选择一个描述性的名称。
标签
布莱恩·D·福伊
brian d foy 是一名 Perl 训练师和作家,同时也是 Perl.com 的资深编辑。他是《Mastering Perl》、《Mojolicious Web Clients》、《Learning Perl Exercises》的作者,以及《Programming Perl》、《Learning Perl》、《Intermediate Perl》和《Effective Perl Programming》的共同作者。
浏览他们的文章
反馈
这篇文章有问题吗?请通过在 GitHub 上打开一个 issue 或 pull request 来帮助我们。