在GNU CLISP 2.49.92中,以下代码:
(defvar i 1)
(defun g ()
(format T "g0:~d~%" i)
(setf i 2)
(format T "g1:~d~%" i))
(defun f ()
(setf i 3)
(format T "f0:~d~%" i)
(g)
(format T "f1:~d~%" i))
(f)
给出以下输出:
f0:3
g0:3
g1:2
f1:2
NIL
同样,C 中的以下代码:
#include <stdio.h>
static int i = 1;
int g (void) {
printf("g0:%dn", i);
i = 2;
printf("g1:%dn", i);
}
int f (void) {
i = 3;
printf("f0:%dn", i);
g();
printf("f1:%dn", i);
}
int main() {
f();
return 0;
}
给出以下输出:
f0:3
g0:3
g1:2
f1:2
根据我找到的文档,defvar
创建了一个动态作用域的特殊变量。另一方面,C 是一种静态范围的语言。然而,这两段代码给出了相同的输出。那么特殊变量和全局变量有什么区别呢?
区别在于特殊变量是动态作用域的:该名称的任何绑定对于在该绑定的动态范围内运行的任何代码都是可见的,无论该绑定在词法上对代码是否可见。
在下文中,我将滑过一些事情:请参阅最后的注释,以获取有关我滑冰的内容的一些提示。
了解绑定和赋值之间的区别很重要,这在各种语言中经常混淆(特别是 Python,但不是真正的 C):
- 用作名词,绑定是名称和值之间的关联;
- 用作动词,绑定变量会在名称和值之间创建新关联;
- 变量的赋值修改现有绑定,它修改名称和值之间的关联。
所以,在 C 中:
void g (void) {
int i; /* a binding */
int j = 2; /* a binding with an initial value */
i = 1; /* an assignment */
{
int i; /* a binding */
i = 3; /* an assignment to the inner binding of i */
j = 4; /* an assignment to the outer binding of j */
}
}
C 将绑定称为"声明"。
在Lisp中(我在这里和下面指的是"Common Lisp"),绑定是由少量原始绑定形式创建的:函数绑定它们的参数,let
建立绑定,也许还有其他一些形式。 现有的绑定最终会被setq
和其他一些运算符改变:setf
是一个宏,在简单的情况下扩展到setq
。
C 没有动态绑定:如果我的g
函数调用某个函数h
那么如果h
尝试引用i
则要么是错误,要么是引用某个全局i
。
但是 Lisp 确实有这样的绑定,尽管默认情况下不使用它们。
因此,如果您采用默认情况,绑定的工作方式与 C 相同(事实上,它们没有,但区别在这里无关紧要):
(defun g ()
(let ((i) ;a binding (initial value will be NIL)
(j 2)) ;a binding with a initial value
(setf i 1) ;an assignment
(let ((i)) ;a binding
(setf i 3) ;an assignment to the inner binding of i
(setf j 4)))) ;an assignment to the outer binding of j
在这种情况下,您只需查看(这就是"词汇"的含义)即可知道哪些绑定是可见的,哪些赋值会改变哪些绑定。
像这样的代码将是一个错误(技术上:是未定义的行为,但我称之为"错误"):
(defun g ()
(let ((i))
(h)))
(defun h ()
(setf i 3)) ;this is an error
这是一个错误,因为(假设没有i
的全局绑定),h
看不到g
建立的绑定,因此无法改变它。 这不是错误:
(defun g ()
(let ((i 2))
(h i)
i))
(defun h (i) ;binds i
(setf i 3)) ;mutates that binding
但是调用g
将返回2
,而不是3
h
因为它正在改变它创建的绑定,而不是创建的绑定g
。
动态绑定的工作方式非常不同。 创建它们的正常方法是使用defvar
(或defparameter
),它声明给定名称是"全局特殊的",这意味着该名称的所有绑定都是动态的(也称为"特殊")。 因此,请考虑以下代码:
;;; Declare *i* globally special and give it an initial value of 1
(defvar *i* 1)
(defun g ()
(let ((*i* 2)) ;dynamically bind *i* to 2
(h)))
(defun h ()
*i*) ;refer to the dynamic value of *i*
呼叫g
将返回2
。 在这种情况下:
;;; Declare *i* globally special and give it an initial value of 1
(defvar *i* 1)
(defun g ()
(let ((*i* 2)) ;dynamically bind *i* to 2
(h)
*i*))
(defun h ()
(setf *i* 4)) ;mutate the current dynamic binding of *i*
调用g
将返回4
,因为h
已经改变了g
建立的*i*
的动态绑定。 这将返回什么?
;;; Declare *i* globally special and give it an initial value of 1
(defvar *i* 1)
(defun g ()
(let ((*i* 2)) ;dynamically bind *i* to 2
(h))
*i*)
(defun h ()
(setf *i* 4)) ;mutate the current dynamic binding of *i*
动态绑定在您希望为计算建立一些动态状态时非常有用。 例如,想象某个处理某种交易的系统。 你可以这样写:
(defvar *current-transaction*)
(defun outer-thing (...)
(let ((*current-transaction* ...))
(inner-thing ...)))
(defun inner-thing (...)
...
refer to *current-transaction* ...)
请注意,*current-transaction*
本质上有点"环境状态":事务动态范围内的任何代码都可以看到它,但您不必花费大量工作将其传递给所有代码。 还要注意,你不能用全局变量来做到这一点:你可能会认为这会起作用:
(defun outer-thing (...)
(setf *current-transaction* ...)
(inner-thing)
(setf *current-transaction* nil))
从表面上看,它会...直到你得到一个错误,*current-transaction*
分配给一些虚假的东西。 好吧,您可以在CL中处理它:
(defun outer-thing (...)
(setf *current-transaction* ...)
(unwind-protect
(inner-thing)
(setf *current-transaction* nil)))
unwind-protect
表格意味着无论是否发生错误,*current-transaction*
总是在离开时被分配给nil
。 这似乎效果更好...直到你开始使用多个线程,此时你会尖叫着死去,因为现在*current-transaction*
在所有线程之间共享,你注定要失败(见下文): 如果你想要动态绑定,你需要动态绑定,事实上,你不能用赋值来伪造它们。
一件重要的事情是,由于 CL 不会在文本上区分动态绑定上的操作和词法绑定上的操作,因此应该有一个关于名称的约定非常重要,因此当您阅读代码时,您可以理解它。 对于全局动态变量,此约定是用*
字符将名称括起来:*foo*
不foo
。 如果您不想陷入混乱的深渊,那么使用此约定很重要。
我希望这足以理解什么是绑定,它们与赋值有何不同,什么是动态绑定以及它们为什么有趣。
<小时 />注释。
- 当然,除了Common Lisp之外,还有其他的Lisp。 它们有不同的规则(例如,在 elisp 中很长一段时间,所有绑定都是动态的)。
- 在CL及其关系中,动态绑定称为"特殊"绑定,因此动态变量是"特殊变量"。 变量可能
- 只是局部的,但动态绑定,尽管我没有讨论过它们。
- 最初,Common Lisp 不支持全局变量和词法变量:所有创建全局变量的构造都创建全局动态(或特殊)变量。 然而,CL足够强大,如果你想要它们,很容易模拟全局词汇。
- 关于赋值给一个未声明的变量(没有明显的绑定)应该做什么,存在一些争议,我在上面提到了这一点。 有些人声称这是可以的:他们是异端,应该被回避。 当然,他们把我当成异端,认为我应该被回避...... 像
(defvar *foo*)
这样的东西有一个微妙之处:这声明*foo*
是一个动态变量,但没有给它一个初始值:它是全局动态的,但全局未绑定。- Common Lisp 没有定义任何类型的线程接口,因此,从技术上讲,特殊变量在线程存在的情况下如何工作是未定义的。 我敢肯定,在实践中,所有具有多个线程的实现都处理我上面描述的特殊绑定,因为其他任何事情都会很糟糕。 一些(也许是所有)实现(也许全部)允许您指定新线程获得一些全局变量的一组新绑定,但这不会改变任何这些。
我还会错过其他事情。
在你显示的情况下,你正在设置一个现有的绑定。 这里没有什么奇怪的。 有趣的部分是当您let
特殊变量时会发生什么。
(defvar *i* 1)
(defun f ()
(format t "f0: ~a~%" *i*)
(let ((*i* (1+ *i*)))
(format t "f1: ~a~%" *i*)
(g)
(incf *i*)
(format t "f2: ~a~%" *i*))
(format t "f3: ~a~%" *i*))
(defun g ()
(incf *i*)
(format t "g: ~a~%" *i*))
(f)
其中打印:
f0: 1
f1: 2
g: 3
f2: 4
f3: 1
*i*
的let
将创建一个动态范围(因为*i*
被全局声明为defvar
)。