特殊变量和全局变量的区别

Difference between Special Variable and Global Variable

在 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:%d\n", i);
  i = 2;
  printf("g1:%d\n", i);  
}

int f (void) {
  i = 3;
  printf("f0:%d\n", i);
  g();
  printf("f1:%d\n", i);  
}

int main() {
    f();
    return 0;
}

给出以下输出:

f0:3
g0:3
g1:2
f1:2

根据我找到的文档,defvar 创建了一个动态作用域的特殊变量。另一方面,C 是一种静态范围的语言。然而,这两段代码给出了相同的输出。那么特殊变量和全局变量有什么区别呢?

在您展示的情况下,您设置的是现有绑定。这里没有什么奇怪的。有趣的部分是当你 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 创建动态 extent(因为 *i*defvar 全局声明为特殊)。

区别在于特殊变量是动态作用域:该名称的任何绑定对于在该绑定的动态范围内运行的任何代码都是可见的,无论是否绑定在词法上对代码可见。

在接下来的内容中,我会滑过一些东西:请参阅末尾的注释以获取有关我滑过的内容的一些提示。

了解 bindingassignment 之间的区别很重要,这在各种语言中经常混淆(特别是 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 调用绑定 'declarations'。

在 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

在这种情况下,您可以通过查看('lexical' 的意思)来判断哪些绑定是可见的,哪些赋值会改变哪些绑定。

像这样的代码会是一个错误(从技术上讲:是未定义的行为,但我会称之为 'an error'):

(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 将 return 2,而不是 3,因为 h 正在改变它创建的绑定,而不是绑定 g 已创建。

动态绑定的工作方式非常不同。创建它们的正常方法是使用 defvar(或 defparameter),它声明给定名称是 'globally special',这意味着 该名称的所有绑定都是动态的(也称为 'special')。所以考虑这个代码:

;;; 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 将 return 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 将 return 4,因为 h 已经改变了由 g 建立的 *i* 的动态绑定。这会是什么return?

;;; 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* 本质上是 'ambient state' 的一点:事务动态范围内的任何代码都可以看到它,但您不必花费大量的工作来通过它归结为所有代码。另请注意,您不能使用全局变量执行此操作:您可能认为这会起作用:

(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 中,all 绑定是动态)。
  • 在CL及其关系中,动态绑定被称为'special'绑定,因此动态变量是'special variables'.
  • 虽然我没有讨论过它们,但可能只有局部变量是动态绑定的。
  • 从根本上讲,Common Lisp 不支持全局变量和词法变量:所有创建全局变量的结构都会创建全局 动态(或特殊)变量。然而 CL 足够强大,如果你想要的话,它很容易模拟全局词法。
  • 关于对未声明变量(没有明显绑定的变量)的赋值应该做什么存在一些争议,我在上面提到过。有些人声称这是可以的:他们是异端,应该被避开。他们当然认为我是异教徒,认为我应该被避开...
  • (defvar *foo*)这样的事情有一个微妙之处:它声明*foo*是一个动态变量,但是没有给它一个初始值: 它是全局动态的,但全局不受约束。
  • Common Lisp 没有定义任何类型的线程接口,因此从技术上讲,特殊变量在线程存在时如何工作是未定义的。我敢肯定,在实践中,所有具有多线程的实现都会像我上面描述的那样处理特殊绑定,因为其他任何事情都会很糟糕。一些(也许是所有)实现(也许是所有)允许您指定新线程获得一些全局变量的一组新绑定,但这不会改变任何这些。

还有其他我错过的东西。