使用Perl循环遍历文件行的最佳防御方式是什么?

15
我通常使用以下代码循环遍历文件中的行:

{{我通常使用以下代码循环遍历文件中的行:}}

open my $fh, '<', $file or die "Could not open file $file for reading: $!\n";
while ( my $line = <$fh> ) {
  ...
}

然而,在回答另一个问题时, Evan Carroll 修改了我的答案,将我的 while 语句改为:

while ( defined( my $line = <$fh> ) ) {
  ...
}

他的理由是,如果你有一行是0(它必须是最后一行,否则它将有一个回车符),那么如果你使用我的语句($line将被设置为"0",并且赋值返回值也将是"0",这将被评估为false),那么你的while会过早退出。如果你检查定义,则不会遇到这个问题。这很有道理。
所以我试了一下。我创建了一个文本文件,其最后一行是没有回车符的0。我通过我的循环运行它,循环没有过早退出。
然后我想,"啊哈,也许这个值实际上不是0,也许还有其他东西搞砸了!"于是我使用了Devel::Peek中的Dump(),它给了我这个:
SV = PV(0x635088) at 0x92f0e8
  REFCNT = 1
  FLAGS = (PADMY,POK,pPOK)
  PV = 0X962600 "0"\0
  CUR = 1
  LEN = 80

那似乎告诉我该值实际上是字符串"0",因为如果我对一个明确设置为"0"的标量调用Dump(),我会得到类似的结果(唯一的区别在于LEN字段——从文件LEN为80,而从标量LEN为8)。
那么怎么回事呢?如果我传递一个没有回车符的只有"0"的行给while()循环,为什么它不会过早退出?Evan的循环是否更加严谨,或者Perl内部是否做了一些疯狂的事情,意味着你不需要担心这些问题,while()实际上只有在遇到eof时才会退出?

1
如果你想编写防御性代码,请使用 坦克 - Matt Ball
3
这就是为什么我不会改变别人回复的意思(只会更正明显的拼写错误),如果你认为有遗漏或可以改进的地方,请添加评论。同时,感谢您对内部情况进行调查! - Ether
3个回答

18

由于

 while (my $line = <$fh>) { ... }

实际上编译成

 while (defined( my $line = <$fh> ) ) { ... }

这可能在很早期的 Perl 版本中是必要的,但现在不再需要了!您可以通过在脚本上运行 B::Deparse 验证此事:

>perl -MO=Deparse
open my $fh, '<', $file or die "Could not open file $file for reading: $!\n";
while ( my $line = <$fh> ) {
  ...
}

^D
die "Could not open file $file for reading: $!\n" unless open my $fh, '<', $file;
while (defined(my $line = <$fh>)) {
    do {
        die 'Unimplemented'
    };
}
- syntax OK

那么你已经准备就绪了!


1
PS,我喜欢...绝对喜欢在5.12及以上版本中...是有效的语法。太喜欢了。 - Robert P
2
不要给出-1,因为我认为“B :: Deparse”在你卡住时探索Perl正在做什么方面是有用的,但在我的观点中,使用它来“回答”这样的问题是不正确的。 它只告诉您在尝试处理的少数几种特定情况下Perl将执行什么操作,它并不告诉您该行为会在什么一般条件下发生。 只有语言规范才能告诉您。 - j_random_hacker
4
@j_random_hacker,由于 Perl5 没有正式的语言规范,解释器的行为就是明确的答案。 - Eric Strom
如果你曾经查看过 Perl 5 邮件列表,在讨论某些条件下的 Perl 行为时,对于语言行为问题的典型回答大致是这样的:“我查看了测试用例,检查了文档,然后‘查看了 C 代码以确认这些是正确的’。” :) - Robert P
2
@j_random_hacker:为Perl辩护,Perl6结束了这一切。有一个规范和几个独立的实现可以进行比较。规范是最终的决定。 - Daenyth
显示剩余8条评论

13

顺便提一下,这在perldoc perlop的I/O操作符部分有介绍:

在标量上下文中,使用尖括号对文件句柄进行求值会产生该文件的下一行(如果有换行符则包括在内),或者在文件末尾或出错时产生“undef”。当$/被设为“undef”(有时称为文件读入模式)且文件为空时,第一次返回‘’,随后返回“undef”。

通常情况下你需要将返回值赋给一个变量,但有一种情况会自动赋值。只有当输入符号是“while”语句条件中唯一的内容(即使伪装成“for(;;)”循环),值会自动赋给全局变量$_,覆盖以前存在的任何东西。(这可能对你来说看起来很奇怪,但你会在几乎每个Perl脚本中使用该结构)。$ _变量不会自动地局部化,如果你想实现这一点,你需要在循环之前放置“local $_;”。

以下行具有相同的效果:

while (defined($_ = <STDIN>)) { print; }
while ($_ = <STDIN>) { print; }
while (<STDIN>) { print; }
for (;<STDIN>;) { print; }
print while defined($_ = <STDIN>);
print while ($_ = <STDIN>);
print while <STDIN>;

这个方法的行为类似,但避免使用 $_ :

while (my $line = <STDIN>) { print $line }
在这些循环结构中,被赋值的变量(无论是自动还是显式赋值)会被测试是否已定义。defined测试避免了在Perl中出现字符串值被视为false的问题,例如一个没有尾随换行符的""或"0"。如果您真的希望这些值终止循环,应该明确地对它们进行测试。
while (($_ = <STDIN>) ne '0') { ... }
while (<STDIN>) { last unless $_; ... }

在其他布尔上下文中,如果启用了"use warnings"编译指示或"-w"命令行开关($^W变量),没有显式的"defined"测试或比较的"<filehandle>"会引发警告。


1
好的答案,所以我删除了我的回答。但是Perl文档有误导性——它们说,“仅当输入符号是while语句条件中唯一的内容时”,但后来通过展示while (my $line = <STDIN>)也表现出相同的方式而与“仅当”部分相矛盾。这让我们想知道在哪些情况下会执行这种DWIMmery。 - j_random_hacker
1
@j_random:“当且仅当”部分是不是指的是$_是否被用作从句柄读取的行的位置,而不是defined逻辑是否被使用? - Ether
你说得完全正确,是我阅读理解能力差。我道歉。我仍然认为明确指出何时自动应用define会更好。我的猜测是:如果循环条件测试是<SOMETHING>或标量赋值的RHS是<SOMETHING> - 这就是全部吗? - j_random_hacker

1

虽然 while (my $line=<$fh>) { ... } 的形式是正确的,它会被编译while (defined( my $line = <$fh> ) ) { ... }。但是请注意,在循环中或测试 <> 返回值时,如果没有显式使用 defined,就有可能会误解读取值为“0”的情况。

以下是几个例子:

#!/usr/bin/perl
use strict; use warnings;

my $str = join "", map { "$_\n" } -10..10;
$str.="0";
my $sep='=' x 10;
my ($fh, $line);

open $fh, '<', \$str or 
     die "could not open in-memory file: $!";

print "$sep Should print:\n$str\n$sep\n";     

#Failure 1:
print 'while ($line=chomp_ln()) { print "$line\n"; }:',
      "\n";
while ($line=chomp_ln()) { print "$line\n"; } #fails on "0"
rewind();
print "$sep\n";

#Failure 2:
print 'while ($line=trim_ln()) { print "$line\n"; }',"\n";
while ($line=trim_ln()) { print "$line\n"; } #fails on "0"
print "$sep\n";
last_char();

#Failure 3:
# fails on last line of "0" 
print 'if(my $l=<$fh>) { print "$l\n" }', "\n";
if(my $l=<$fh>) { print "$l\n" } 
print "$sep\n";
last_char();

#Failure 4 and no Perl warning:
print 'print "$_\n" if <$fh>;',"\n";
print "$_\n" if <$fh>; #fails to print;
print "$sep\n";
last_char();

#Failure 5
# fails on last line of "0" with no Perl warning
print 'if($line=<$fh>) { print $line; }', "\n";
if($line=<$fh>) { 
    print $line; 
} else {
    print "READ ERROR: That was supposed to be the last line!\n";
}    
print "BUT, line read really was: \"$line\"", "\n\n";

sub chomp_ln {
# if I have "warnings", Perl says:
#    Value of <HANDLE> construct can be "0"; test with defined() 
    if($line=<$fh>) {
        chomp $line ;
        return $line;
    }
    return undef;
}

sub trim_ln {
# if I have "warnings", Perl says:
#    Value of <HANDLE> construct can be "0"; test with defined() 
    if (my $line=<$fh>) {
        $line =~ s/^\s+//;
        $line =~ s/\s+$//;
        return $line;
    }
    return undef;

}

sub rewind {
    seek ($fh, 0, 0) or 
        die "Cannot seek on in-memory file: $!";
}

sub last_char {
    seek($fh, -1, 2) or
       die "Cannot seek on in-memory file: $!";
}
我并不是在说这些是 Perl 的好形式! 我只是在说它们是可能的;特别是第三、四和五种失败情况。请注意第四和第五种情况没有 Perl 警告的失败。前两种情况也有它们自己的问题...

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