Lexical Scoping and Dynamic Scoping

我相信每一位程序员在学习或者实践编程时,都会遇到作用域的问题。您可能觉得作用域很简单,甚至都不能当做一个问题来看待,我想原因可能是目前主流的计算机语言Java, Python, C等等几乎一致采用Lexical Scoping,程序员没有机会察觉到异样,从而掩盖了还有其他作用域的事实,许多教科书也很配合地一笔带过,当然还有一种可能是您已经洞察了不同作用域的差异和内涵,自觉地规避了问题。我衷心的希望所有程序员都是第二种可能。

本文将浅显地描述两种作用域的特点及差异,希望可以解释地清楚一点,如果读完之后您更加模糊了,请记住,这并非我的本意。

在计算机编程中,名称绑定的作用域(名称与实体的关联,比如变量)是程序中名称绑定有效的部分,也就是说,该名称可以被引用到一个实体。程序的其他部分,名称可能指向不同的实体(它可能具有不同的绑定),或者根本什么都没有(它可能是未绑定的)。作用域也称为实体的可见性, 尤其在较早的技术文献中可见,这是从所引用的实体的角度出发,而非引用的名称。

术语"scope“也用于指代程序中有效的名称绑定的那部分集合,更加正确的称呼应该是上下文 或者 环境


Lexical Scoping

词法作用域,某些书中也叫静态作用域,其在编写代码时或者说定义时就确定的,通过文本(源代码)就可以观察到名称与实体的关联,程序运行时会在距离被调用代码最近的环境中去查找绑定,如果存在多个环境,在优先从最里面一层的环境查找。

来看一个例子:

(setf x 1)                      ;; bind x to 1
(setq f
    (let ((x 2))                ;; bind x to 2
        (lambda (y) (* x y))))

(funcall f 3) ;; result is 6

(format t "X = ~d " x) ;; X = 1

先绑定x为1,然后定义一个函数f,f中的变量x绑定为2,实际上f形成了一个闭包,由于两个x的环境不同,所以互不干扰,结果正是我们期望的,这就属于词法作用域。

Dynamic Scoping

动态作用域曾经让很多人深恶痛绝,以至于不愿意再提起它。早期的Lisp普遍使用了动态作用域,并带来了非常严重的问题,这也是Lisp Machine被Unix打败的除了商业因素之外的重要原因,Lisp至今未成为主流语言就不难理解了,尽管Lisp的语法最精炼、最优美,还具有最强悍的宏系统,还是世界上第一个使用GC的语言…

庆幸的是,Scheme, Common Lisp等Lisp方言已经都是用了Lexial Scoping(局部变量。所以某些时候我们可以把Lexical Scoping和Local Variable等价), 我们无需再为一些莫名其妙的问题而烦恼,珍惜生命,不是吗? 不过Scheme, Common Lisp还是保留了动态作用域的操作,Scheme可以参考Fluid Binding, Common Lisp的动态作用域将在后文举例说明,尽管如此,我们无需为这个保留担忧,因为基本上我们很少需要使用到这个功能。

所谓dynamic scoping就是说,在函数定义中存在了“自由变量”(free variable), 会在运行时随着函数的“调用位置”不同而发生变化,还是用上面的例子来说明,但是稍作变化:

(defvar x 1)                    ;; bind x to 1, x is a free variable
(setq f
    (let ((x 2))                ;; bind x to 2
        (lambda (y) (* x y))))

(funcall f 3)
(format t "X = ~d " x)
(let ((x 100)) (format t "Inner X is ~d " x))

(defvar x 1)定义了x,值为1,但是这个x属于自由变量,处于动态作用域,因为Lisp自动将全局变量即由defvar、defparameter、special定义的变量自动声明为动态作用域。 x对于(lambda (y) (* x y))来说是“ 自由 ” 的,它可不会在意x绑定为2或者任何其他值,因为(let ((x 2)) …)中指定的x已经不是(lambda …)的参数了。

所以当我们执行(funcall f 3) 时,其结果不像词法作用域那样等于6,而是3

(format t “X = ~d " x) 依然输出: X = 1

(let ((x 100)) (format t “Inner X is ~d " x)) 输出为:Inner X is 100。 等等,这里的x为什么又是100了? 还记得上文提到的环境 吗?!

如果将函数f放到不同的位置执行会发生什么? 上面(funcall f 3)是在顶层执行的,我们换到let中执行会怎么样?

(let ((x 2)) (funcall f 3))

这次居然返回6,而不是3了。

看到问题了吗?函数f的行为,会随着调用位置的一个名叫x的变量的值的不同而变化。虽然都叫x,但并不是同一个变量,只是名字相同而已。顺带提一句,现在可以理解Java中namespace的用处了吧。


词法作用域是通过搜索本地词法上下文解决的,而动态作用域是通过搜索本地执行上下文(即位置)解决的。这是编译器必须面对并解决的问题,或许编写程序时无需过多关注,但是多了解一下一定大有裨益。尽管动态作用域存在很大问题,但它并非洪水猛兽,它可以使全局变量更易于管理;假设您希望临时改变自由变量的值时,也可以声明为dynamic socping;另外在异常处理中也可以使用动态作用域将处理程序与异常相关联。