Perl中的缺省变量$_
2019-04-29
周溱
用过perl的人对缺省变量 $_ 都一定相当熟悉.由于有了缺省变量,许多写法可以相当简化.例如:
while (<>) { print; }
tr/a-z/A-Z/
chomp
但使用不当,你可能也会碰到相当费解的 bug. 下面我们仔细探究一下使用 $_ 的内在细节,希望能避免 bug, 更高效地写出正确的程序.
案例
我前一段写了个小程序,大致如下:
foreach (@filenames) {
my $fh = open($_);
while (<$fh>) {
s/good/bad/g;
}
close($h);
say "$_ fixed!";
}
有人看出问题了吗?我估计 perl 要六级以上才能看出问题.提示:打印出来的东西完全不是你一开始想的,更严重的是 @filenames 内的数据被破坏.究竟是怎么回事?
variable scopes
Perl 提供丰富的 variable scope 规则:
- global variable
- dynamically-scoped variable
- lexically-scoped variable
搞懂这些scoping rule, 不仅对 perl 程序员有用,对所有程序员都有用.
global scoped
其实就是没有scope, 即全局变量.这最简单,可以理解成有一个全局的字典,通过名字指向静态分配的内存地址.在程序中的任意地方你都可以通过同一个名字读写同一块内存.perl 中的 our 变量就是全局变量.
dynamically-scoped
global scoped 简单,但导致程序各部分相互干扰很大.于是发明了局部变量,和 scope 关联.dynamically-scoped 的方法是维持全局字典,但在进入一个 scope 的时候,把全局字典中一部分另行保存,改成这个 scope 自定义的新值.离开这个 scope 的时候再恢复回去,保证对外界没有干扰.在 perl 里, local 变量就是 dynamically-scoped 局部变量.
lexically-scoped
还有另外一种方法.就是每个 scope 自己定义自己的一个小字典.先查自己小字典,查不到了,再查全局大字典.这也实现了局部重定义变量.在 perl 里, my 变量就是 lexically-scoped 局部变量.
dynamical vs lexical
假如你只有一个全局 scope 加一个局部 scope, 那这两者没本质区别.但如果再多,就有区别了.lexically-scoped 永远只作用在本 scope 内,和你程序如何执行没有关系.但 dynamically-scoped 和程序运行路径相关,会影响之后你程序运行遇见的所有新的 scope. 例如:
our $a = 1;
our $b = 2;
sub inner_print {
say "a=$a, b=$b";
}
say "a=$a, b=$b"; # a=1, b=2
OUTER: {
my $a = 3;
local $b = 4;
say "a=$a, b=$b"; # a=3, b=4
inner_print(); # a=1, b=4
}
在之上的例子里,由于 $b 在 OUTER scope 是 dynamically-scoped, 而 $a 是 lexically-scoped, 导致在 inner_print 里一个用全局值,一个用 OUTER 里的值.
通常而言,静态编译的程序更适合使用 lexically-scoped, 而动态解释的程序更适合使用 dynamically-scoped. 例如,在 C 语言里并无 dynamically-scoped 的方法.但 perl 及其他高级语言给了你更多的能力,相应的,也就有更多的可能性.某些功能用 dynamically-scoped 变量更方便,但对于大多数情况, lexically-scoped 更不容易出错,也更容易被编译器优化.所以在你不清楚该用什么的时候,请先考虑 lexically-scoped.
$_ 是什么
我们再回到一开始,理解一下 $_ 究竟是什么.它的使用分为大致两个方面:
- 提供者.包括 iterator (例如 foreach) 和 stream 输入 (例如 while(<>) ).perl 的术语叫:topicalizer
- 缺省使用者.包括 regular expression matching, 一些系统自带函数等.
由于以上因素,可以推理出:
- $_ 是全局变量
- $_ 无法做 lexically-scoped 局部变量
- $_ 可以做 dynamically-scoped 局部变量
很简单,假如 $_ 做了 lexically-scoped 局部变量, 它就无法自然传递到下层函数,那它的方便性就没了一大半,也就没意义了.让我们再考虑一下在 while(<>) 和 foreach() 两种用法下,究竟发生了什么.
while (<>) 里的 $_
当我们写:
while(<$fh>) {
}
实际上 perl 会给我们自动扩写成:
while(defined($_ = <$fh>) {
}
这里,全局变量 $_ 会被赋值,会影响到外面.在我们最初的例子里,内循环的 while 改写了它. 同时由于下面 foreach 的性质,改写了外循环遍历的数组 @filenames.由于 while 循环的结束条件,最终它和 @filenames 全都变成了 undefined.
foreach 里的 $_
当我们写:
foreach (@filenames) {
}
实际上 perl 会给我们自动扩写成:
{
local $_;
my $i;
for($i=0; $i<scalar(@filenames); \$_=\$filenames[$i], $i++) {
}
}
这里和上面 while 有两个显著区别:
- $_ 并非拷贝了数组里的数据,而是一个 alias, 指向数组里的真实数据
- perl 自动生成一个外层,并将 $_ 做成一个 dynamically-scoped 的局部变量
为什么 $_ 必须是一个 alias呢?第一避免了内存拷贝,第二保证把它当作 l-value 可以直接修改原数组里的值.下面代码才成为可能.
foreach (@filenames) {
s/good/bad/g;
}
上面这段代码会修改 @filenames 里的内容,这是预期的结果.这和 while (<>) 不一样,在 while (<>) 里数据拷贝是天然且合理的,输入流并无修改的需要.
由于 $_ 必须是一个 alias, 所以 perl 必须再做一个外部 scope 把它做成局部变量.假如不这样做的话,foreach 循环结束之后,它将是一个无效的 alias,对它的任何操作都会导致内存越界.这和 while (<>) 结束之后残留一个无效数据不一样:无效的指针比无效的数据危险性大的多.
怎么避免
从上面的分析来说,foreach 不污染环境,但可能受污染,while (<>) 污染环境但不容易受污染.问题出在 while (<>) 上.那为什么 while (<>) 不也使用自动外层和 dynamically-scoped 呢?我想这主要是历史原因,而且从上面的分析来说, while (<>) 的污染是可预测的.对于我们程序员来说,为避免这个坑,你要记住以下两条就够了.
不做内循环
while (<>) 只能做最外侧循环.如果要做内循环,可以改成 lexically-scoped, 不用 $_:
while (defined(my $line=<$h>)) {
}
这样做的缺点是没有缺省变量了,不够方便.或者也可以用内部 dynamically-scoped $_:
while (1) {
local $_ = <$fh>;
last unless (defined($_));
}
这样做的缺点是感觉罗嗦一点.
保护外层
除非你的 while (<>) 已经在程序顶层,即不在任何函数内部,尽量在 while (<>) 之外把 $_ 定义成 dynamically-scoped 局部变量来保护你函数的上层调用:
sub my_func {
local $_;
while(<$fh>) {
}
}
如果你的程序的 while (<>) 在最顶层最外侧循环,则无需做任何改变.这其实也就是 perl 原来简化写法的初衷.
总结
perl 让你写出精简方便的代码,但这后面的原理并不简单.理解 dynamically-scoped 和 lexically-scoped 的区别不仅让你能更深的掌握perl, 对其他高级语言的学习也很有帮助.
There is more than one way to do it. --- Larry Wall