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 函数的默认变量,例如 chompsplit。你可以通过在 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(以及其他循环结构)的操作:lastnextredo

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;

你可以用这些做更多的事情。它们与标签和嵌套循环一起工作。你可以在 perlsynLearning 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 来帮助我们。