在Perl中,使用foreach和while迭代文件有什么区别?

34

我在Perl中有一个文件句柄FILE,我想迭代文件中的所有行。以下两种方式有什么区别吗?

while (<FILE>) {
    # do something
}

并且。
foreach (<FILE>) {
    # do something
}
8个回答

40

大多数情况下,您可能不会注意到任何区别。但是,foreach 会将每一行读入一个列表中(而不是数组),然后逐行进行处理,而while则一次读取一行。由于foreach 在迭代文件行时会使用更多的内存和需要较长的处理时间,因此通常建议使用while

编辑(通过Schwern):foreach循环等同于以下内容:

my @lines = <$fh>;
for my $line (@lines) {
    ...
}

不幸的是,Perl没有像它对范围运算符(1..10)那样优化这种特殊情况。

比如,如果我使用for循环和while循环读取 /usr/share/dict/words 文件,并在完成后让它们休眠,我可以使用ps查看该进程消耗了多少内存。 作为对照,我还包含了一个只打开文件但不做任何事情的程序。

USER       PID %CPU %MEM      VSZ    RSS   TT  STAT STARTED      TIME COMMAND
schwern  73019   0.0  1.6   625552  33688 s000  S     2:47PM   0:00.24 perl -wle open my $fh, shift; for(<$fh>) { 1 } print "Done";  sleep 999 /usr/share/dict/words
schwern  73018   0.0  0.1   601096   1236 s000  S     2:46PM   0:00.09 perl -wle open my $fh, shift; while(<$fh>) { 1 } print "Done";  sleep 999 /usr/share/dict/words
schwern  73081   0.0  0.1   601096   1168 s000  S     2:55PM   0:00.00 perl -wle open my $fh, shift; print "Done";  sleep 999 /usr/share/dict/words

for循环程序将近消耗了32 MB的实际内存(RSS列)来存储我2.4 MB的/usr/share/dict/words文件的内容。而while循环每次仅存储一行,只消耗70 KB用于行缓冲。


数组和列表之间的区别非常重要。混淆它们会导致你的理解和最终代码出现错误。 - daotoad
2
-1 直到你提到 while (<FILE>) {} 会践踏 $,而 foreach 不会(因为 foreach 首先将 $ 局部化)。这肯定是最重要的行为差异! - j_random_hacker
4
内存差异更加重要和实用。无论如何,您都不应该仅依赖于$ _来编写大量代码,因为许多事情都会破坏它。 - Schwern
@Schwern:如果你看不出默默更改常用全局变量的代码会导致维护噩梦,我怀疑你是否曾经参与过大型项目。内存使用仅在极少数情况下对于真正巨大的文件才重要;否则,操作系统的虚拟内存管理器将确保一切正常(但速度较慢)。 - j_random_hacker
1
使用列表时,“foreach”有另一个含义。如果文件句柄是保存另一个程序输出的管道,则“foreach”将等待管道关闭,因为只有这样它才能确保将所有行读入数组中,而“while”仅在发送“\ n”(或更准确地说,输入记录分隔符字符)到管道上之后才会阻塞。使用“while”允许您在程序完成运行之前处理其中一个程序的输出。 - Nathan Fellman
显示剩余7条评论

19
在标量上下文中(即while),<FILE>按顺序返回每一行。
在列表上下文中(即foreach),<FILE>返回由文件中每一行组成的列表。
应该使用while结构。
有关更多信息,请参见perlop - I/O运算符
编辑:j_random_hacker正确地指出
while (<FILE>) { … }

在 foreach 循环中,会先将 $_ 局部化,而不会对其进行修改。而 trample on 就是对 $_ 进行修改。毫无疑问,这是最重要的行为差异!


1
-1 直到你提到 while (<FILE>) {} 会践踏 $,而 foreach 不会(因为 foreach 首先将 $ 局部化)。毫无疑问,这是最重要的行为差异! - j_random_hacker
1
谢谢!这种不直观的差异是相当多错误的根源。 - j_random_hacker
1
@j_random_hacker,您能详细说明一下while循环如何“践踏”$_,而foreach则将其局部化的含义吗?还有我需要注意哪些注意事项?我是Perl初学者,正在努力掌握基础知识... - Alby
2
@Alby:在代码 $_ = 42; foreach (@some_list) { ... } 中,由于Perl在这种情况下自动定位了 $_,所以 $_ 的值为42。但是在 $_ = 42; while (<FILE>) { ... }之后,$_是从 FILE 中读取的最后一行文本(通常情况下是undef,表示已经读完整个文件)。这很让人恼火,因为foreach的行为更安全/更易维护,但使用foreach读取文件意味着要先将整个文件读入内存,如果您有一个大文件,逐行处理就足够了,这将非常浪费内存! - j_random_hacker
1
非常感谢。这完全有意义。如果有一个循环机制可以逐行读取文件处理程序并仍然本地化 $_,那就太好了。 - Alby
如果你真的需要那个,我相信你可以用以下方式包装你的 while 循环:{local $_; while (...)},这应该能解决问题。 - insaner

11

除了之前的回答外,使用while的另一个好处是您可以使用$.变量。这是最后一个访问的文件句柄的当前行号(请参阅perldoc perlvar)。

while ( my $line = <FILE> ) {
    if ( $line =~ /some_target/ ) {
        print "Found some_target at line $.\n";
    }
}

关于“accessed”,具体来说,可以通过以下方式:readline/glob(又名<>),eof,tell,sysseek。 - ysth
严格来说,您也可以使用for循环访问$.变量;但是由于它首先完全展开列表,因此您始终会得到最后一行的行号。 - brunov

4
我在Effective Perl Programming的下一版中添加了一个相关示例。
使用while,您可以停止处理FILE,并仍然获得未处理的行:
 while( <FILE> ) {  # scalar context
      last if ...;
      }
 my $line = <FILE>; # still lines left

如果您使用foreach,即使停止处理它们,您也会消耗foreach中的所有行:
 foreach( <FILE> ) { # list context
      last if ...;
      }
 my $line = <FILE>; # no lines left!

3

j_random_hacker这个答案的评论中提到了这一点,但实际上并没有单独回答,尽管这是另一个值得注意的区别。

区别在于while (<FILE>) {}会覆盖$_,而foreach(<FILE>) {}会局部化它。也就是说:

$_ = 100;
while (<FILE>) {
    # $_ gets each line in turn
    # do something with the file
}
print $_; # yes I know that $_ is unneeded here, but 
          # I'm trying to write clear code for the example

将会打印出<FILE>的最后一行。

然而,
$_ = 100;
foreach(<FILE>) {
    # $_ gets each line in turn
    # do something with the file
}
print $_;

将英文翻译成中文:

将打印出100。要使用while(<FILE>) {}结构获得相同的结果,您需要执行以下操作:

$_ = 100;
{
    local $_;
    while (<FILE>) {
        # $_ gets each line in turn
        # do something with the file
    }
}
print $_; # yes I know that $_ is unneeded here, but 
          # I'm trying to write clear code for the example

现在这将打印 100

3

更新:评论中的 j random hacker 指出,当从文件句柄读取时,Perl 会特殊处理 while 循环中的假测试。我已经验证了读取 false 值不会终止循环——至少在现代 Perl 中是这样的。抱歉给大家带来困扰。写了 15 年 Perl 的我还是个新手。 ;)

以上所有人都是正确的:使用 while 循环更加节省内存并且能够给你更多的控制。

关于 while 循环的一个有趣的事情是,它会在读取 false 时退出。通常这将是文件结尾,但如果它返回一个空字符串或 0 呢?糟糕!你的程序就会过早退出。如果文件的最后一行没有换行符,这可能会发生在任何文件句柄上。如果自定义文件对象具有不像常规 Perl 文件对象那样处理换行符的读取方法,也可能会发生这种情况。

以下是如何解决此问题。检查是否读取了未定义的值,这表示已到达文件结尾:

while (defined(my $line = <FILE>)) {
    print $line;
}
< p > 顺便说一下,< code > foreach 循环没有这个问题,即使效率低下也是正确的。


2
不!Perl会特殊处理形式为"while (<FILE>) { ... }"的代码,使其与您建议的替换方式完全相同:"while (defined($_ = <FILE>)) {}"。 因此,在文件末尾只包含“0”而没有LF字符的行将不会被忽略。请参见perlop中的"I/O运算符"部分。 - j_random_hacker
太好了!这个问题是什么时候解决的?Pod 中仍然有很多示例使用 while defined 语法。如果我没记错的话,Perl 曾经对 while(<>) 和 while(<FILE>) 的处理方式不同。 - Ken Fox
2
据我所知,自Perl 5以来一直是这样的,但我不确定。而且Perl对它将特殊处理的形式非常挑剔:例如,“while(<>)”,“while($ _ = <FILE>)”和“while(my $ x = <FILE>)”被特殊处理,但“while($ _ ='' . <FILE>)”则不是。(使用以“0”结尾且没有LF的文件进行测试。) - j_random_hacker
不要担心感觉像个新手......我从1999年开始使用Perl,一个月前才发现范围运算符对于两个常量标量进行了特殊处理!(例如,“1..10”) :) 是的,有些POD文档过时了,而且谷歌搜索结果中也会出现一些错误的建议/解释。 - j_random_hacker
我撤回了我给你的-1,但如果你提到"while (<FILE>)"会践踏$_,而"foreach (<FILE>)"会将$_局部化,避免了践踏,你仍然可以从我这里得到另一个+1。这种行为上的不明显差异会导致相当多的微妙错误。 - j_random_hacker

1

这里有一个例子,foreach不能工作,但是while可以完成任务。

while (<FILE>) {
   $line1 = $_;
   if ($line1 =~ /SOMETHING/) {
      $line2 = <FILE>;
      if (line2 =~ /SOMETHING ELSE/) {
         print "I found SOMETHING and SOMETHING ELSE in consecutive lines\n";
         exit();
      }
   }
}

使用foreach是无法做到这一点的,因为它会在进入循环之前将整个文件读入列表中,你将无法在循环内读取下一行。我相信即使在foreach中也会有解决这个问题的方法(比如读入数组),但while绝对提供了一个非常直接的解决方案。

第二个例子是当你需要解析一台机器上的大型文件(比如3GB),而该机器只有2GB RAM时。foreach将会耗尽内存并崩溃。我在我的perl编程生涯早期就是通过这种艰难的方式学习到了这一点。


0

foreach循环比while循环(基于条件)更快。


foreach 循环也是基于条件的。它的条件是在列表上完成工作。 - Nathan Fellman

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接