静态作用域与动态作用域
目录

1. 例子

首先定义函数foo,然后调用:

;;;; field.lisp
(let ((y 7))
(defun foo (x)
(print x)
(print y)))

(let ((y 5))
(foo 1))

使用sbcl和emacs来执行:

> sbcl --script field.lisp
1
7
> emacs --script field.lisp
1
5

很明显,这两个解释器的执行结果是不同的。

造成以上结果不同的原因和变量的绑定(binding)有关。 绑定代表了变量在运行时期的存在,存在相应就有作用域(scope)和生存期(extent)。 简单说:

  • common lisp采用的是静态作用域。在foo中寻找y的绑定时,它检查函数foo的定义环境
  • emacs lisp采用的是动态作用域。在foo中寻找y的绑定时,它检查函数foo的执行环境

2. 静态作用域

典型:C, C++, Python, Java, JavaScript, Lua,大多数现在程序设计语言都是采用静态作用域规则。

静态作用域又叫做词法作用域,使用词法作用域的变量叫词法(lexical)变量。 词法变量都有一个确定的作用域和不确定的生存期。 词法变量的作用域可以是一个函数或block,使得其在这段代码区域内都有效。 但是词法变量的生存期取决于该变量需要引用(reference)多久(?)。

词法作用域里,对于函数体中的一个符号,不会逐层检查函数的调用链,而是检查函数定义时的外部环境,即捕捉的是函数定义时该符号的绑定。

在例子中的第一个let表里,定义了一个变量,符号名为y并绑定了值7,那么这个y的作用域就是这个let表区域。 foo函数定义在这个区域内,其内部会使用到一个符号名为y的变量。 那么在词法作用域的情况下,当foo被调用时,其会查找其定义的环境有没有符号名y的变量可以绑定,如果有则把foo中符号y的值绑定,在这里就是7。 并且这里foo中的y和外部let中的y共享一个值,都是对这个值的引用,并不是拷贝了一个新值。把例子中的代码做如下改变:

;;;; field2.lisp
(let ((y 7))
(defun foo (x)
(print x)
(print y)
(setq y (+ y 2))))

(let ((y 5))
(foo 1)
(foo 1))

(let ((y 5))
(foo 2))
> sbcl --script field2.lisp
1
7
1
9
2
11

这说明前后两次调用foo的过程中,y共享着同一个值。

最后要说明的是,Common Lisp中的词法变量虽然类似于某些语言中的局部变量,这些局部变量并没有提供类似Common Lisp词法变量的所有功能,尤其是并非所有语言都提供了支持闭包的词法作用域变量。

3. 动态作用域

典型:Emacs Lisp, Common Lisp(两种都有), Perl(两种都有)。

使用动态作用域的变量叫做动态(dynamic)变量,有时也叫做特殊(special)变量。 动态变量具有不确定的作用域,只取决于在什么地方绑定它。 同时从绑定开始,到绑定时的代码段执行完毕,这就是动态变量的生存期。

动态作用域里,函数执行遇到一个符号,会由内向外逐层检查函数的调用链,并打印第一次遇到的那个绑定的值。显然,最外层的绑定即是全局状态下的那个值。

在例子中的第二个let表里,同样定义一个变量,符号名为y并绑定到值5。那么这个y的作用域是在这个(第二个)let表及其调用的函数里。 在动态作用域下,其内部调用的函数foo中的y并不是检查其定义的环境,而是检查调用的环境,首先检查foo内部有没有y的定义,没有的话检查调用其的第二个let区域内有没有y的定义,发现有,则绑定到值5上面。为了说明问题,把代码也做改变:

;;; field3.lisp
(let ((y 7))
(defun foo (x)
(print x)
(print y)
(setq y (+ y 2))))

(let ((y 5))
(foo 1)
(foo 1))

(let ((y 5))
(foo 2))

(foo 1)
> emacs --script field3.lisp
1
5
1
7
2
5
1
Symbol's value as variable is void: y

第二个let区域内y的值是共享的,所以第二次调用foo会使用第一次+2的结果。 而第三个let区域重新定义了y值,而这次调用foo会绑定到这个新值。

因为ELisp只能在调用栈中查找变量,因此ELisp不支持闭包。

参考

 于 2014-05-20 02:13 时评论:
解释的很清楚,多谢。

发表评论