Lisp中的setq和defvar是什么?

59

我看到Practical Common Lisp使用(defvar *db* nil)来设置全局变量。使用setq达到同样的目的不可以吗?

使用defvarsetq相比,有什么优缺点呢?

4个回答

59

有几种引入变量的方式。

DEFVARDEFPARAMETER引入全局动态变量。 DEFVAR可选地将其设置为某个值,除非已定义。 DEFPARAMETER始终将其设置为提供的值。 SETQ不引入变量。

(defparameter *number-of-processes* 10)

(defvar *world* (make-world))     ; the world is made only once.

请注意,您可能不希望使用诸如xystreamlimit等名称来DEFVAR变量。为什么?因为这些变量将被声明为特殊变量,难以撤消。特殊声明是全局的,变量的所有后续使用都将使用动态绑定。

不好的例子:

(defvar x 10)     ; global special variable X, naming convention violated
(defvar y 20)     ; global special variable Y, naming convention violated

(defun foo ()
  (+ x y))        ; refers to special variables X and y

(defun bar (x y)  ; OOPS!! X and Y are special variables
                  ; even though they are parameters of a function!
  (+ (foo) x y))

(bar 5 7)         ; ->   24

BETTER: 在变量名称中使用 * 标记特殊变量!

(defvar *x* 10)     ; global special variable *X*
(defvar *y* 20)     ; global special variable *Y*

(defun foo ()
  (+ *x* *y*))      ; refers to special variables X and y

(defun bar (x y)    ; Yep! X and Y are lexical variables
  (+ (foo) x y))

(bar 5 7)           ;  ->   42

局部变量是通过DEFUN, LAMBDA, LET, MULTIPLE-VALUE-BIND等方式引入的。

(defun foo (i-am-a-local-variable)
   (print i-am-a-local-variable))

(let ((i-am-also-a-local-variable 'hehe))
  (print i-am-also-a-local-variable))

现在,默认情况下,上述两种形式中的局部变量是词法变量,除非它们被声明为。然后它们将成为动态变量。
接下来,还有几种形式可以将变量设置为新值。 SETSETQSETF和其他形式。SETQSETF 可以设置词法和特殊(动态)变量。
对于可移植代码,要求设置已经声明的变量。未声明变量的确切效果由标准定义为未定义。
因此,如果您知道您的Common Lisp实现正在做什么,您可以使用
(setq world (make-new-world))

在顶层的读取-求值-输出循环中使用。但不要在你的代码中使用,因为其效果不具备可移植性。通常SETQ将设置变量。但是一些实现可能会在不知道它时声明变量为SPECIAL(CMU Common Lisp默认情况下就是这样)。这几乎总是不是人们想要的。如果你知道自己在做什么,可以在非正式场合使用它,但不要用于代码中。
同样适用于此处:
(defun make-shiny-new-world ()
  (setq world (make-world 'shiny)))

首先,这些变量应该写成*world*(用周围的*字符),以明确它是全局特殊变量。其次,在声明之前应该使用DEFVARDEFPARAMETER进行声明。
一个典型的Lisp编译器会抱怨上述变量未声明。由于Common Lisp中不存在全局词法变量,编译器必须为动态查找生成代码。一些编译器会说,好吧,我们假设这是一个动态查找,让我们将其声明为special - 因为这是我们的假设。

2
我很惊讶地发现,一旦你使用defvar声明变量为全局变量后,就无法创建同名的局部变量了。因此,如果defvar已经声明了x,那么(let ((x 1)) x)可能会产生意外的结果。 - Ian
4
这就是为什么很多人会使用“耳罩”(也就是从不使用(defvar x foo),而是使用(defvar *x* foo),这样你犯错的可能性要小得多)的原因之一。 - Vatine

23

defvar引入一个动态变量,而setq用于为动态或词法变量赋值。动态变量的值在调用函数的环境中查找,而词法变量的值在定义函数的环境中查找。以下示例将清楚地说明差异:

;; dynamic variable sample
> (defvar *x* 100)
*X*
> (defun fx () *x*)
FX
> (fx)
100
> (let ((*x* 500)) (fx)) ;; gets the value of *x* from the dynamic scope.
500
> (fx) ;; *x* now refers to the global binding.
100

;; example of using a lexical variable
> (let ((y 200))
   (let ((fy (lambda () (format t "~a~%" y))))
     (funcall fy) ;; => 200
     (let ((y 500))
       (funcall fy) ;; => 200, the value of lexically bound y
       (setq y 500) ;; => y in the current environment is modified
       (funcall fy)) ;; => 200, the value of lexically bound y, which was 
                     ;; unaffected by setq
     (setq y 500) => ;; value of the original y is modified.
     (funcall fy))) ;; => 500, the new value of y in fy's defining environment.

动态变量在传递默认值时非常有用。例如,我们可以将动态变量*out*绑定到标准输出,使其成为所有io函数的默认输出。要覆盖此行为,我们只需引入一个本地绑定:
> (defun my-print (s)
        (format *out* "~a~%" s))
MY-PRINT
> (my-print "hello")
hello
> (let ((*out* some-stream))
    (my-print " cruel ")) ;; goes to some-stream
> (my-print " world.")
world

词法变量的一个常见用途是定义闭包,以模拟具有状态的对象。在第一个示例中,fy的绑定环境中的变量y有效地成为该函数的私有状态。

defvar仅在变量未被分配时才会将值分配给变量。因此,对*x*的以下重新定义不会更改原始绑定:

> (defvar *x* 400)
*X*
> *x*
100

我们可以使用setq*x*分配一个新值:
> (setq *x* 400)
400
> *x*
400
> (fx)
400
> (let ((*x* 500)) (fx)) ;; setq changed the binding of *x*, but 
                         ;; its dynamic property still remains.
500
> (fx)
400

5
很遗憾,这是错误的。在未声明/定义的变量上执行(setq y 200)的确切影响是不确定的。Common Lisp也没有全局词法变量。SETQ只会设置一个变量,不会有其他影响。它会根据提供的变量是动态变量还是词法变量而有所不同。LET用于绑定变量,SETQ用于设置变量。 - Rainer Joswig
同时,由于标准函数已经使用了名称CL:PRINT,因此不能定义函数CL:PRINT。FORMAT是用于流输出而非文件输出的。 - Rainer Joswig
@Rainer 感谢您指出不准确之处。我已经更新了答案。 - Vijay Mathew

9

DEFVAR 建立一个新的变量。 SETQ 给一个变量赋值。

我使用过的大多数 Lisp 实现,如果你给一个不存在的变量 SETQ 赋值,将会发出一个警告。


8
defvardefparameter都引入全局变量。正如Ken所指出的那样,setq分配给一个变量。
此外,defvar不会覆盖先前被defvar定义过的内容。Seibel在本书后面(第6章)中说:“实际上,您应该使用DEFVAR来定义变量,这些变量将包含数据,即使您对使用变量的源代码进行更改,您仍希望保留这些数据。”
例如,在Simple Database章节中,如果您有一个名为*db*的全局数据库: http://www.gigamonkeys.com/book/variables.html
(defvar *db* nil)

如果你在REPL中开始使用它,添加、删除等操作,但是当你更改包含defvar表单的源文件时,重新加载该文件不会清除*db*和你可能进行的所有更改... 我相信setqdefparameter会清除它。如果我错了,希望有更有经验的Lisper来指正。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接