6.7 动态作用域 (Dynamic Scope)
2.11 小节解释过局部与全局变量的差别。实际的差别是词法作用域(lexical scope)的词法变量(lexical variable),与动态作用域(dynamic scope)的特别变量(special variable)的区别。但这俩几乎是没有区别,因为局部变量几乎总是是词法变量,而全局变量总是是特别变量。
在词法作用域下,一个符号引用到上下文中符号名字出现的地方。局部变量缺省有着词法作用域。所以如果我们在一个环境里定义一个函数,其中有一个变量叫做 x
,
(let ((x 10))
(defun foo ()
x))
则无论 foo
被调用时有存在其它的 x
,主体内的 x
都会引用到那个变量:
> (let ((x 20)) (foo))
10
而动态作用域,我们在环境中函数被调用的地方寻找变量。要使一个变量是动态作用域的,我们需要在任何它出现的上下文中声明它是 special
。如果我们这样定义 foo
:
(let ((x 10))
(defun foo ()
(declare (special x))
x))
则函数内的 x
就不再引用到函数定义里的那个词法变量,但会引用到函数被调用时,当下所存在的任何特别变量 x
:
> (let ((x 20))
(declare (special x))
(foo))
20
新的变量被创建出来之后, 一个 declare
调用可以在代码的任何地方出现。 special
声明是独一无二的,因为它可以改变程序的行为。 13 章将讨论其它种类的声明。所有其它的声明,只是给编译器的建议;或许可以使程序运行的更快,但不会改变程序的行为。
通过在顶层调用 setf
来配置全局变量,是隐式地将变量声明为特殊变量:
> (setf x 30)
30
> (foo)
30
在一个文件里的代码,如果你不想依赖隐式的特殊声明,可以使用 defparameter
取代,让程序看起来更简洁。
动态作用域什么时候会派上用场呢?通常用来暂时给某个全局变量赋新值。举例来说,有 11 个变量来控制对象印出的方式,包括了 *print-base*
,缺省是 10
。如果你想要用 16 进制显示数字,你可以重新绑定 *print-base*
:
> (let ((*print-base* 16))
(princ 32))
20
32
这里显示了两件事情,由 princ
产生的输出,以及它所返回的值。他们代表着同样的数字,第一次在被印出时,用 16 进制显示,而第二次,因为在 let
表达式外部,所以是用十进制显示,因为 *print-base*
回到之前的数值, 10
。