LET和SETQ的区别是什么?

32

我正在使用GCL在Ubuntu上编程。根据来自各种来源的Common Lisp文档,我了解到let创建本地变量,而setq设置现有变量的值。 在下面的情况中,我需要创建两个变量并对它们的值求和。

使用setq

(defun add_using_setq ()
  (setq a 3)  ; a never existed before , but still I'm able to assign value, what is its scope?
  (setq b 4)  ; b never existed before, but still I'm able to assign value, what is its scope?
  (+ a b))

使用 let

(defun add_using_let ( )
  (let ((x 3) (y 4)) ; creating variables x and y
     (+ x y)))
在这两种情况下,我似乎都能达到相同的结果。在使用setqlet之间有什么区别?为什么不能在需要使用let的所有地方都使用setq(因为它在语法上更容易)?

1
如果 setq 在语法上很容易,为什么它会让代码的读者产生疑问,比如“它的作用域是什么?” - Kaz
4个回答

38

setq给变量赋值,而let引入新的变量/绑定。例如,在以下情况下会发生什么:

(let ((x 3))
  (print x)      ; a
  (let ((x 89))
    (print x)    ; b
    (setq x 73)  
    (print x))   ; c
  (print x))     ; d


3   ; a
89  ; b
73  ; c
3   ; d

外部的let创建了一个本地变量x,而内部的let创建了另一个遮蔽内部变量的本地变量。请注意,使用let来遮蔽变量不会影响被遮蔽变量的值;第d行中的x是外部let引入的x,其值没有改变。setq只影响调用它的变量。这个例子展示了在本地变量中使用setq,但它也可以与特殊变量一起使用(意思是动态作用域,通常使用defparameterdefvar定义):

CL-USER> (defparameter *foo* 34)
*FOO*
CL-USER> (setq *foo* 93)
93
CL-USER> *foo*
93

请注意,setq并不(可移植地)创建变量,而letdefvardefparameter等则会。当使用一个尚未是变量的参数调用setq时,其行为未定义,具体由实现决定如何处理。例如,SBCL会大声抱怨:
CL-USER> (setq new-x 89)

; in: SETQ NEW-X
;     (SETQ NEW-X 89)
; 
; caught WARNING:
;   undefined variable: NEW-X
; 
; compilation unit finished
;   Undefined variable:
;     NEW-X
;   caught 1 WARNING condition
89

Of course,更好地理解这些概念的最佳方法是阅读和编写更多的Lisp代码(随着时间的增长),并阅读HyperSpec中的条目并遵循交叉引用,特别是词汇表条目。例如,HyperSpec中对setqlet的简短描述包括:
  • SETQ

    变量分配值。

  • LET

    let 和 let* 创建新的变量绑定并执行一系列使用这些绑定的表单。

您可能希望阅读更多关于变量和绑定的内容。 letlet* 在动态变量和 special 声明方面也有一些特殊行为(但您可能需要一段时间才能了解这些内容),在某些情况下(您可能不需要一段时间了解这些情况),当一个变量实际上不是变量时,setq 实际上等同于 setf。详见 HyperSpec。

在 Stack Overflow 上有一些并非完全重复的问题,但可能有助于理解 Common Lisp 中可用的各种变量定义和赋值运算符的使用:


1
值得注意的是,尽管SBCL(和CCL)会抱怨代码中setq未定义的符号,但事实上它们仍然会定义该符号,从而产生了@wvxvw答案中描述的行为,当运行add_using_setq时。虽然这种行为不是标准要求的,但它有着悠久的历史。我有一些旧教科书,教你使用setq来定义全局变量。 - Mars
1
想一想,我不确定在顶层之外使用 setq 的影响是否有悠久的历史。也许不是。 - Mars

5

在函数定义内部绑定变量时,应该始终使用let关键字 - 除非您希望该值可供同一作用域内的其他函数使用。

我喜欢emacs lisp手册中的描述:

let用于将符号与值绑定起来,以使Lisp解释器不会将变量与同名的不属于函数的变量混淆。

要了解为什么需要let特殊形式,请考虑您拥有一个通常称为“房子”的家,例如句子“房子需要粉刷”。如果您正在访问朋友,您的主人提到“房子”,他可能是指他的房子,而不是你的房子,也就是说,是另一间房子。

如果您的朋友正在指他的房子,而您认为他正在指您的房子,那么您可能会感到困惑。如果在一个函数内部使用的变量与在另一个函数内部使用的同名变量不打算引用同一个值,则可能会发生类似的情况。let特殊形式可以防止这种混淆。

-- http://www.gnu.org/software/emacs/manual/html_node/eintr/let.html


2

(setq x y)将一个新值y分配给由符号x指定的变量,并可选择定义一个新的包级别变量1。这意味着在调用add_using_setq之后,您将在当前包中有两个新的包级别变量。

(add_using_setq)
(format t "~&~s, ~s" a b)

输出结果为3 4,这并不是期望的结果。

相比之下,当您使用let时,只会在函数的执行期间将新值分配给由符号指定的变量,因此此代码将导致错误:

(add_using_let)
(format t "~&~s, ~s" a b)

let想象成等同于以下代码:

(defun add-using-lambda ()
  (funcall (lambda (a b) (+ a b)) 3 4))

顺便提一下,你真的应该查看其他程序员编写的代码,以了解如何命名或格式化事物。除了传统之外,它还具有一些排版属性,您真的不想失去。

1 这种行为是非标准的,但在许多流行的实现中都会发生。尽管它相当可预测,但出于其他原因,它被认为是一种不良习惯,主要是所有会劝阻您使用全局变量的相同问题。


"(setq x y) 全局地将一个新值 y 分配给由符号 x 指定的变量" 这种说法是不正确的。 setq 用于为变量分配一个值,而变量可以是局部的(词法作用域,例如使用 let 并且未声明为 special)或全局的(例如使用 defparameterdefvar 定义)。 - Joshua Taylor
只要我们在挑刺 :), 讨论“包级变量”也没有太多意义。词法变量在源代码中由符号标识,但没必要在编译后保留命名变量的符号。另一方面,特殊变量由符号标识,虽然符号可以有一个包,但这是变量可能与包相关联的唯一意义。如果实现使 (setq foo::x 34) 定义一个特殊变量,则符号 foo::x 可在任何地方访问。(不是故意找茬,只是说话口气随意) - Joshua Taylor
2
“Special variable”是Common Lisp中的一个技术术语;它们通常被称为动态变量或动态作用域变量。重要的是要称它们为special,因为_语言_称它们为特殊变量。例如,请参见[special declaration](http://www.lispworks.com/documentation/HyperSpec/Body/d_specia.htm#special)上的条目,以及[_special variable_]的词汇表条目(http://www.lispworks.com/documentation/HyperSpec/Body/26_glo_s.htm#special_variable)。这是在搜索`hyperspec“special variable”`时Google提供的第一个结果。 :) - Joshua Taylor
1
@wvxvw:我完全支持Joshua Taylor的反对意见。 "Package-level"这个术语会让人完全误解这些变量的行为。它们不是词法变量,其词法作用域是它们所在的包。它们是动态变量,因此它们的绑定存在于一个范围而不是一个作用域中。而且,如果某个变量是由一些深度嵌套的let和相应的special声明引入的,那么这个变量到底是什么级别的呢? - Rörd
1
@wvxvw:问题在于“包级别”并不能告诉你这些变量的“特殊之处”,即它们是动态变量,let会为那些let的动态范围而改变它们的值,而不是它们的词法作用域。更好的理解是,defvardefpackage全局定义一个符号作为变量的名称,并且(内部)符号属于包(对于变量名使用未内部化的符号没有太多意义)。 - Rörd
显示剩余14条评论

0
  • SETQ

只要Lisp仍在运行,您就可以从作用域中获取符号的值(它将值分配给符号)。

  • LET

在Lisp完成评估表单后,您无法获取使用LET定义的符号的值(它将值绑定到符号并创建符号的新绑定)。

考虑下面的例子:

;; with setq
CL-USER> (setq a 10)
CL-USER> a
10

;; with let
CL-USER> (let ((b 20))
       (print b))
CL-USER> 20
CL-USER> b ; it will fail
; Evaluation aborted on #<UNBOUND-VARIABLE B {1003AC1563}>.
CL-USER>

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