为啥 elisp 局部变量在这种情况下保持其值?

Posted

技术标签:

【中文标题】为啥 elisp 局部变量在这种情况下保持其值?【英文标题】:Why does an elisp local variable keep its value in this case?为什么 elisp 局部变量在这种情况下保持其值? 【发布时间】:2013-05-16 06:40:52 【问题描述】:

有人可以向我解释一下这个非常简单的代码 sn-p 中发生了什么吗?

(defun test-a ()
  (let ((x '(nil)))
    (setcar x (cons 1 (car x)))
    x))

第一次调用(test-a) 时,我得到了预期的结果:((1))。 但令我惊讶的是,再次调用它,我得到((1 1))((1 1 1)) 等等。 为什么会这样?期望(test-a) 总是返回((1)) 我错了吗? 另请注意,重新评估test-a的定义后,返回结果会重置。

还要考虑这个功能按我的预期工作:

(defun test-b ()
  (let ((x '(nil)))
    (setq x (cons (cons 1 (car x)) 
                  (cdr x)))))

(test-b) 总是返回 ((1))。 为什么test-atest-b 不等价?

【问题讨论】:

【参考方案1】:

看起来你的 (let) 中的 '(nil) 只被评估了一次。当您(setcar)时,每个调用都在原地修改相同的列表。如果将 '(nil) 替换为 (list (list)),则可以使 (test-a) 工作,尽管我认为有一种更优雅的方法。

(test-b) 每次都从 cons 单元中构造一个全新的列表,这就是它工作方式不同的原因。

【讨论】:

谢谢,您的回答确实提供了解决方法,但我仍然不明白为什么会这样。 'test-a 中的 x 被声明为本地的,实际上它在函数外部不可见,但为什么函数在调用之间保留有关 x 的信息? 猜测,我会说这是因为emacs在解析let中的文字时构造了列表,然后每次调用函数时都引用同一个列表。这就像它使用指向解析树的指针来引用 x。 @abo-abo:emacs保留关于 x 的信息,而是关于它的初始值。看我的回答。【参考方案2】:

坏人

test-a自修改代码。这是非常危险的。虽然变量 xlet 表单的末尾消失,但它的初始值 保留在函数对象中,这就是您正在修改的值。请记住,在 Lisp 中 a function is a first class object 可以传递(就像数字或列表一样),有时也可以修改。这正是您在这里所做的:x 的初始值是函数对象的一部分,您正在修改它。

让我们实际看看发生了什么:

(symbol-function 'test-a)
=> (lambda nil (let ((x (quote (nil)))) (setcar x (cons 1 (car x))) x))
(test-a)
=> ((1))
(symbol-function 'test-a)
=> (lambda nil (let ((x (quote ((1))))) (setcar x (cons 1 (car x))) x))
(test-a)
=> ((1 1))
(symbol-function 'test-a)
=> (lambda nil (let ((x (quote ((1 1))))) (setcar x (cons 1 (car x))) x))
(test-a)
=> ((1 1 1))
(symbol-function 'test-a)
=> (lambda nil (let ((x (quote ((1 1 1))))) (setcar x (cons 1 (car x))) x))

test-b 返回一个新的 cons 单元格,因此是安全的。 x 的初始值永远不会被修改。 (setcar x ...)(setq x ...)的区别在于前者修改了已经存储在变量x中的object,而后者stores一个new strong> x 中的对象。区别类似于x.setField(42)x = new MyObject(42) 中的x = new MyObject(42)

底线

一般情况下,最好将quoted 数据(如'(1))视为常量-不要修改它们:

quote 返回参数,而不计算它。 (quote x) 产生 x警告quote 不构造它的返回值,而只是返回 Lisp 阅读器预先构造的值(请参阅信息节点 Printed Representation)。这意味着(a . b) 不是 等同于(cons 'a 'b):前者没有缺点。报价应该 保留给永远不会被副作用修改的常量, 除非你喜欢自修改代码。查看信息中的常见陷阱 节点Rearrangement 的意外结果示例 引用的对象被修改。

如果您需要modify a list,请使用listconscopy-list 而不是quote 创建它。

见moreexamples。

PS。这已在 Emacs 上复制。

PPS。另请参阅 Why does this function return a different value every time? 以了解相同的 Common Lisp 问题。

【讨论】:

但是为什么修改仍然存在?我认为这里只是指向'(nil) 的变量x 不应该在let 表单的末尾删除吗? @Tyler:变量被删除,但初始值没有。见编辑。 既然您已经解释过了,那就说得通了。我永远不会建立这种联系。 在定义test-a 之后调用(symbol-function 'test-a) 以及在调用它之后再次调用它是很有启发性的。 @abo-abo 在test-a中,setcar用来直接修改x指向的列表。 X 指向函数定义中的常量,因此函数本身会发生变化。在 test-b 中,列表中 x 最初指向的值用于构造要分配给 x 的新列表。初始值永远不会直接改变。所以你是对的,setcar 是关键的区别。【参考方案3】:

我发现罪魁祸首确实是'quote。这是它的文档字符串:

返回参数,不计算它。

...

警告:`quote' 不构造它的返回值,而只是返回 Lisp 阅读器预先构建的值

...

引号应该保留给那些将 永远不要被副作用修改,除非你喜欢自我修改代码。

为了方便,我也重写了

(setq test-a 
      (lambda () ((lambda (x) (setcar x (cons 1 (car x))) x) (quote (nil)))))

然后用了

(funcall test-a)

看看 'test-a 是如何变化的。

【讨论】:

以上是关于为啥 elisp 局部变量在这种情况下保持其值?的主要内容,如果未能解决你的问题,请参考以下文章

什么是线程局部变量?

什么是线程局部变量?

什么是线程局部变量?

最有用的java面试题

elisp语法

MATLAB中定义的persistent变量为啥会显示两种颜色?