六、Perl语言的诡异之处
虽然我们经常看电视,但是很少人会吃饱了撑着,把说明书读个遍,然后把电视机大卸八块,进行一番研究。但程序设计语言和电视机可不一样,你学得越多用得越多,就越想知道它的工作内幕。我被迫成了Perl语言正经八百的学习者后,大学时那种好奇心又回来了。
上个世纪六七十年代,有两个语言特性,被计算机科学界认为是有害的,应该从程序设计语言中清除出去。一个大家都知道,goto语句;还有一个可能见过的人不多,dynamic scope(动态作用域)。
再看Perl语言,好嘛,两个都有。在早期Perl中,动态作用域甚至是唯一的作用域规则。这Larry要是到计算机系读博士,如果有程序设计语言理论这门课,估计会被授课教授拍死。回想起linux之父Linus Tovalds和操作系统大腕 Andy Tanenbaum教授之间那场关于微内核的争论,你不得不说,黑客们都是脑后有反骨的,特别敢于争鸣。
看一个简单的Perl代码段:
sub work {
if ($debug > 0) {
print"Debug: Danger!\en";
}
}
注意那个$debug变量,它既非过程参数,也不是局部变量,可能在work过程之外的其他地方声明,套用逻辑学术语算个“自由”变量吧。脱离了具体语境,你无法搞清$debug什么意思。静态动用域和动态作用域在确定“自由”变量的含义方面,有很大的不同。现在补充一些程序片段:
$debug=0;
sub doit {
local $debug=1;
work();
other();
}
调用doit过程,结果会怎样?如果不熟悉Perl语言,按其他常见程序设计语言静态作用域的规则,会把local猜测为doit过程里的局部变量声明,因此对work过程是没有影响的。work过程中的$debug标识符应该引用的是过程外部的全局变量声明。所以不会有任何输出显示。
但是Perl的动态作用域不是这样工作的,local不是局部变量声明,它并没有创建新的局部变量,而是先保存了全局变量$debug的当前值0,再将其赋值为1。由此影响了后面调用的work过程和other过程的行为。在退出当前程序块时,它会自动将$debug变量值恢复为0。可以把$debug想象为一个堆栈,在程序执行过程中,如果过程调用链上有多个过程对同一个变量使用local声明,则堆栈也随之增减。
假如other过程是这样定义的:
sub other {
local $debug=0;
work();
}
那么运行到other过程内部时,想象中的$debug变量堆栈将有三层,从远到近分别是0、1、0,受最近的变量值影响,这里调用的work过程将不会有显示输出。退出other过程后,$debug的值就自动又变为1。在只有静态作用域的语言中模拟这个过程,就没有这么优雅了。程序员必须手工管理这个堆栈。
还有,Perl为什么要在debug标识符前加一个难看的$符号?这是Perl语言设计中另一个迷。在大多数语言中,一个标识符同一时刻只和一个变量进行绑定。但是Perl语言的标识符,可以同时与多种变量进行绑定。比如,$debug代表标量变量,@debug代表数组变量,%debug代表哈希表变量,此外还有好几种。它们都使用debug标识符,只能依靠一个前导符号来区分了。在内部实现中,每个Perl模块(包)中,都有一张符号表(Perl语言称之为typeglob)。符号表中每个标识符,都对应着一组指针。每个位置的指针,如果不空,都指向一种类型的变量。
经过分析,我做了一个大胆的揣测。Perl语言设计可能从古老的Lisp语言中学了不少东西,什么原子、符号表、动态作用域、垃圾收集等等,Larry也许是个Lisp爱好者呢。有人说Javascript=C+Lisp,我看Perl更象,骨子里是个小小的Lisp解释器。
这些特性是如此的诡异,不符合一般的软件工程原则,以致大多数Perl程序员都不敢用。大家还是老老实实的用my声明词法变量吧,反正现在Perl也支持静态作用域和闭包了。标识符虽然可以多绑定,不同类型的变量还是乖乖取不同名字吧,尽量避免混淆。谁让我们不是黑客呢。