0

在 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 是一种静态范围的语言。然而,这两段代码给出了相同的输出。那么特殊变量和全局变量有什么区别呢?

4

2 回答 2

3

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

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

了解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 将绑定称为“声明”。

在 Lisp 中(我在这里和下面的意思是“Common Lisp”),绑定是由少数原始绑定形式创建的:函数绑定它们的参数,let建立绑定,也许还有一些其他形式。现有的绑定最终setq可能会被其他一些运算符改变:是一个在简单情况下setf扩展为的宏。setq

C 没有动态绑定:如果我的g函数调用了某个函数h,那么如果h试图引用i它,要么是错误,要么是引用了一些 global 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(or 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的动态绑定。这将返回什么?*i*g

;;; 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*not foo。如果您不想陷入混乱,使用此约定很重要。

我希望这足以理解什么是绑定,它们与赋值有何不同,以及动态绑定是什么以及它们为什么有趣。


笔记。

  • 当然,除了 Common Lisp 之外,还有其他的 lisp。它们有不同的规则(例如,长期以来,在 elisp 中,所有绑定都是动态的)。
  • 在 CL 及其关系中,动态绑定称为“特殊”绑定,因此动态变量是“特殊变量”。
  • 有可能只有局部变量但动态绑定,尽管我没有讨论过它们。
  • 最初,Common Lisp 不支持既是全局变量又是词法变量:所有创建全局变量的结构都创建全局动态(或特殊)变量。然而 CL 足够强大,如果你想要它们,它很容易模拟全局词汇。
  • 关于对未声明变量(没有明显绑定的变量)的赋值应该做什么存在一些争议,我在上面提到过。有些人声称这没关系:他们是异端,应该被避开。当然,他们认为我是异端,认为我应该被回避……
  • 像这样的事情有一个微妙之处(defvar *foo*): this 声明这*foo*是一个动态变量,但没有给它一个初始值:它是全局动态的,但全局未绑定。
  • Common Lisp 没有定义任何类型的线程接口,因此从技术上讲,特殊变量在线程存在的情况下如何工作是未定义的。我确信在实践中,所有具有多个线程的实现都处理我上面描述的特殊绑定,因为其他任何事情都会很糟糕。一些(可能是全部)实现(可能全部)允许您指定新线程获取一些全局变量的一组新绑定,但这不会改变任何这些。

我还会错过其他一些事情。

于 2022-02-11T12:14:19.397 回答
3

在您展示的情况下,您正在设置现有绑定。这里没有什么令人惊讶的。有趣的部分是当您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

letof*i*创建一个动态范围(因为由*i*全局声明为特殊defvar)。

于 2022-02-11T09:00:16.320 回答