logo一言堂

Perl中的缺省变量$_

用过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