【问题标题】: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 不等价?

【问题讨论】:

    标签: emacs lisp elisp literals


    【解决方案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 是如何变化的。

    【讨论】:

      猜你喜欢
      • 2021-08-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-10-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多