7.2 作用域的演变
作用域是指名字的有效范围。要保证程序整体不会出现名字冲突是件困难的事情,为此将名字的有效范围限定在更小的范围之内,让程序管理变得轻松一些。作用域的提出就是基于这个考虑。前面讲到的问题点,就是在函数 shori 中名字 i 所赋有的值被改写的情况下,会给其他部分的程序带来影响。如果函数 shori 中的名字 i 仅在此函数中有效就好了。这样一来,就只需关注 i 在函数 shori 中的情况就可以了。那么,该如何实现这一点呢?
动态作用域
如何操作
一种解决方法是,把变量原来的值事先保存在函数入口处,在出口处写回变量中。
Perl
sub shori{
$old_i = $i; ❶
$i = 0; # 各种处理
$i = $old_i; ❷
}
函数的开头把变量 i 的值代入变量 old_i 中(❶),函数结束时再把变量 old_i 中的值返回给变量 i(❷。这是一种先把原来的值另存起来随后返回的方法。如此一来,在 ❶ 和 ❷ 之间不管怎样改写 $i 的值,对其他部分都没有任何影响。
使用这种方法,必须要注意不要漏写返回原来的值的那一行❷。函数中有 return 返回时,也要记得先把变量原来的值返回后再执行 return 语句。凡当函数退出时,所有地方都要毫无遗漏地加上返回值的代码5。
5从函数中退出时保证无遗漏地执行某些特定的操作,这个和异常处理结合在一起时,会变成一个复杂的话题。关于这一点,第 6 章有详细的介绍。
然而人总是会犯错误的,对于这样的任务,我们总是希望能让计算机去完成。从 1991 年发布的 Perl 4 开始,Perl 语言增加了这一功能,通过把变量声明为 local 就可以让程序处理器去承担“把原来的值另存起来随后返回”的任务。
Perl
sub shori{
local $i;
$i = 0;
}
这样的作用域称为动态作用域。
问题点
动态作用域有个难以处理之处,在改写了变量之后调用其他函数时,被调用的函数会受到影响。文字说明比较难理解,我们来看以下代码,这里函数 yobu 调用了函数 yobareru。
Perl
$x = "global";
sub yobu{
local $x = "yobu";
&yobareru();
}
sub yobareru{
print "$x\n";
# ↑输出「yobu」
}
&yobu();
在函 yobu 中,变量值被改写为 yobu,在函数 yobareru 中显示变量 x 时,显示出来的是 yobu。就是说,函数 yobu 中的变更对被调用的函数 yobareru 有影响。
引入了动态作用域后,函 yobu 中针对变量的变更对全局变量 x 没有影响,但是这个变更对函数 yobu 里进一步调用的函数 yobareru 有影响。
动态作用域中被改写的值会影响到被调用函数,因此在引用变量时它是什么样的值,不看函数调用方的代码这个是无从得知的。而逐行去看调用方代码会很麻烦。这里举的例子中代码较短,一看就能明白,但在一般情况下,函数在哪里被调用这个信息分散在代码中,是很难把握的 6。
6反过来,如果要用函数调用处确定的值来改变被调用函数的行为该如何实现?参数传递是一种方法。使用参数传递后,读代码的人就知道这个变量的值是在调用处确定的。
这个问题该如何解决呢?
静态作用域
在全局作用域和动态作用域的情况下,跨越程序整体的变量内容对照表为多个函数共用,如同多人同时作业时共用白板一样。因此,函数 yobu 中改写的内容也能立刻被函数 yobareru 读取到。改写的值能被读取到是件好事,但同时也存在不希望读取到改写的值的情况。这就是动态作用域的问题点。该如何解决这一问题呢?
为函数 yobu 中临时使用的变量创建的变量内容对照表,被放在了共用的空间里,因此大家都能读取到。如果不这样做会怎样?即每调用一个函数时就创建新的对照表。就像大家每个人都使用各自桌面上的便签纸,而不是在共用的白板上做记录一样。
动态作用域中的对照表能被全部代码读取
首先,我们来看动态作用域中的代码行为。最初全局对照表中记录了“x 等于 global”(图 7.2)。

图 7.2 写入全局对照表
函数 yobu 中也使用了相同名字的变量 x,怎么做到不改写原来的全局变量 x 中的值呢?之前我们讨论过,把原来的值通过另一个变量名保存起来,之后再写回原来的变量名,用这种方法可以解决。通过创建新的对照表同样可以解决(图 7.3)。

图 7.3 动态作用域下,将记录写入一张全局可见的新的对照表中
使用动态作用域定义局部变量 x,要执行以下三个操作。
进入函数 yobu 时,准备新的对照表。
函数 yobu 写入变量 x 的值记录在该对照表中。
退出函数 yobu 时,作废该张对照表。
动态作用域中创建的对照表可以被全体的源代码读写,这是和静态作用域的一点很大的区别。
执行到函数 yobareru 中参照变量 x 时,还未退出函数 yobu,该张新的对照表还有效。于是程序首先读取这张新的对照表,返回其中记录的内容 yobu(图 7.4)。需要访问该张对照表中没有记录的变量时,就要翻转到下一张对照表,也就是全局对照表进行查找。

图 7.4 参照变量时按照由近及远的顺序读取
静态作用域按函数区分对照表
多个函数共用一张对照表,这是动态作用域的问题点。那就按函数来区分对照表吧。此时程序执行以下三个操作。
进入函数 yobu 时,准备函数 yobu 专用的新的对照表。
函数 yobu 写入变量 x 的值记录在该对照表中。
退出函数 yobu 时,作废该张对照表 7。
7为了不招致误解需要说明一下。在从函数 yobu 中退出前,又一次进入函数 yobu 的情况也有,这时会如何?为了不使二次执行给首次执行带来影响,需要为二次调用创建第二张对照表。这里说明的三个操作是在没有递归调用的情况下退出函数 yobu 前不会再次调用函数 yobu。但一般情况下存在多次递归调用的可能性,这就需要不止一张对照表,而是要为每次函数调用配备一张对照表。
在函数 yobu 中改写变量 x 的值时,该记录被写入函数 yobu 专用的对照表中(图 7.5)。

图 7.5 静态作用域中函数各自的对照表
随后执行到函数 yobareru 中 参照变量 x 时,要访问的不是函数 yobu 专用的对照表,而是函数 yobareru 专用的对照表。这张对照表中因为没有写入函数 yobu 改写的变量值,所以全局变量的值会被读取出来(图 7.6)。
用代码来表示就是下面的样子。
Perl
$x = "global";
sub yobu{
my $x = "yobu"; # 此处从local改为my
&yobareru();
}
sub yobareru{
print "$x\n";
# ↑输出「global」
}
&yobu();
这个例子实现了一种有效作用范围,使得函数 yobu 中的变更不会影响到函数 yobu 之外的部分。它借助的就是静态作用域 8。
8静态作用域也叫字面作用域(lexical scope),因为函数 yobu 中创建的变量的有效范围(作用域)与函数 yobu 在字面上范围是一致的。
这个不是很普通吗?估计有很多人会这样想。没错!现在很多语言都采用了静态作用域。

图 7.6 yobareru 本地没有定义名字 x 故读取全局变量
相对于只有全局作用域的状态,动态作用域体现了一种进步。有了动态作用域,不再需要担心万一别处也使用了名字 i,也不再需要为避免冲突而把自己使用的变量名写在稿纸上,可以安心地使用简短的变量名。然而,动态作用域也有无能为力的地方,为解决这个问题开发出了静态作用域。
现在说全局变量不好或谈避免全局污染的原因就在于此。但是,明明有减小作用范围的标准功能不用而致使问题产生,这是非常不明智的。
