6.7 动态作用域 (Dynamic Sc​​ope)

2.11 小节解释过局部与全局变量的差别。实际的差别是词法作用域(lexical scope)的词法变量(lexical variable),与动态作用域(dynamic scope)的特别变量(special variable)的区别。但这俩几乎是没有区别,因为局部变量几乎总是是词法变量,而全局变量总是是特别变量。

在词法作用域下,一个符号引用到上下文中符号名字出现的地方。局部变量缺省有着词法作用域。所以如果我们在一个环境里定义一个函数,其中有一个变量叫做 x

  1. (let ((x 10))
  2. (defun foo ()
  3. x))

则无论 foo 被调用时有存在其它的 x ,主体内的 x 都会引用到那个变量:

  1. > (let ((x 20)) (foo))
  2. 10

而动态作用域,我们在环境中函数被调用的地方寻找变量。要使一个变量是动态作用域的,我们需要在任何它出现的上下文中声明它是 special 。如果我们这样定义 foo

  1. (let ((x 10))
  2. (defun foo ()
  3. (declare (special x))
  4. x))

则函数内的 x 就不再引用到函数定义里的那个词法变量,但会引用到函数被调用时,当下所存在的任何特别变量 x :

  1. > (let ((x 20))
  2. (declare (special x))
  3. (foo))
  4. 20

新的变量被创建出来之后, 一个 declare 调用可以在代码的任何地方出现。 special 声明是独一无二的,因为它可以改变程序的行为。 13 章将讨论其它种类的声明。所有其它的声明,只是给编译器的建议;或许可以使程序运行的更快,但不会改变程序的行为。

通过在顶层调用 setf 来配置全局变量,是隐式地将变量声明为特殊变量:

  1. > (setf x 30)
  2. 30
  3. > (foo)
  4. 30

在一个文件里的代码,如果你不想依赖隐式的特殊声明,可以使用 defparameter 取代,让程序看起来更简洁。

动态作用域什么时候会派上用场呢?通常用来暂时给某个全局变量赋新值。举例来说,有 11 个变量来控制对象印出的方式,包括了 *print-base* ,缺省是 10 。如果你想要用 16 进制显示数字,你可以重新绑定 *print-base* :

  1. > (let ((*print-base* 16))
  2. (princ 32))
  3. 20
  4. 32

这里显示了两件事情,由 princ 产生的输出,以及它所返回的值。他们代表着同样的数字,第一次在被印出时,用 16 进制显示,而第二次,因为在 let 表达式外部,所以是用十进制显示,因为 *print-base* 回到之前的数值, 10