【问题标题】:In common-lisp, how do I modify part of a list parameter from within a function without changing the original list?在 common-lisp 中,如何在不更改原始列表的情况下从函数中修改列表参数的一部分?
【发布时间】:2010-12-01 20:15:16
【问题描述】:

我正在尝试将列表传递给 Lisp 中的函数,并在函数中更改该列表的内容而不影响原始列表。我读过 Lisp 是按值传递的,这是真的,但还有一些我不太明白的事情。例如,此代码按预期工作:

(defun test ()
    (setf original '(a b c))
    (modify original)
    (print original))
(defun modify (n)
    (setf n '(x y z))
    n)

如果您调用 (test),即使 (modify) 返回 (x y z),它也会打印 (a b c)。

但是,如果您尝试仅更改列表的一部分,则不会这样。我认为这与具有相同内容的列表在任何地方的内存中都相同或类似的东西有关?这是一个例子:

(defun test ()
    (setf original '(a b c))
    (modify original)
    (print original))
(defun modify (n)
    (setf (first n) 'x)
    n)

然后(测试)打印(x b c)。那么如何更改函数中列表参数的某些元素,就好像该列表是该函数的本地一样?

【问题讨论】:

  • 请注意,修改文字常量的后果是不确定的。不要那样做。绝不。 '(a b c) 是代码中的文字常量。你不应该修改它。您可以修改使用函数 LIST 创建的列表,例如 (list 'a 'b 'c) 。
  • 还要注意 (SETF original '(a b c)) 没有意义。 SETF 不引入变量。变量'original'没有定义。可以设置已经通过 LET、DEFUN、DEFVAR、DEFPARAMETER、...引入的变量。

标签: function lisp common-lisp pass-by-reference parameter-passing


【解决方案1】:

这与 C 语言中的示例几乎相同:

void modify1(char *p) {
    p = "hi";
}

void modify2(char *p) {
    p[0] = 'h';
}

在这两种情况下都会传递一个指针,如果更改指针,则更改指针值的参数副本(它在堆栈上),如果更改内容,则更改任何值对象被指向。

【讨论】:

  • 我希望有一本书能尽可能用 C 语言解释等价操作。C 程序员会更快地掌握 Common Lisp 的某些方面。
【解决方案2】:

Lisp 列表基于 cons 单元格。变量就像指向 cons 单元(或其他 Lisp 对象)的指针。改变一个变量不会改变其他变量。更改 cons 单元格将在所有引用这些 cons 单元格的地方可见。

Touretzky 是一本好书,Common Lisp: A Gentle Introduction to Symbolic Computation

还有绘制列表树和 cons 单元格的软件。

如果您将列表传递给这样的函数:

(modify (list 1 2 3))

那么你有三种不同的方式来使用列表:

cons细胞的破坏性修饰

(defun modify (list)
   (setf (first list) 'foo)) ; This sets the CAR of the first cons cell to 'foo .

结构共享

(defun modify (list)
   (cons 'bar (rest list)))

Above 返回一个与传入列表共享结构的列表:两个列表中的其余元素相同。

复制

(defun modify (list)
   (cons 'baz (copy-list (rest list))))

上面的函数BAZ类似于BAR,但没有共享列表单元格,因为列表是复制的。

不用说,通常应该避免破坏性修改,除非有真正的理由这样做(比如在值得时节省内存)。

注意事项:

切勿破坏性地修改文字常量列表

不要做: (let ((l '(a b c))) (setf (first l) 'bar))

原因:该列表可能被写保护,或者可能与其他列表共享(由编译器安排)等。

还有:

引入变量

喜欢这个

(let ((original (list 'a 'b 'c)))
   (setf (first original) 'bar))

或者像这样

(defun foo (original-list)
   (setf (first original-list) 'bar))

永远不要 SETF 未定义的变量。

【讨论】:

  • 谢谢。这对我很有帮助。
【解决方案3】:

SETF 修改了一个地点n 可以是一个地方。列表n指向的第一个元素也可以是一个地方。

在这两种情况下,original 持有的列表作为其参数n 传递给modify。这意味着函数test 中的original 和函数modify 中的n 现在都拥有相同的列表,这意味着originaln 现在都指向它的第一个元素。

在 SETF 在第一种情况下修改 n 后,它不再指向该列表,而是指向一个新列表。 original 指向的列表不受影响。新列表随后由modify 返回,但由于此值未分配给任何内容,因此它会逐渐消失,很快就会被垃圾回收。

在第二种情况下,SETF 修改的不是n,而是n 指向的列表的第一个元素。这是original指向的同一个列表,所以之后你也可以通过这个变量看到修改后的列表。

要复制列表,请使用COPY-LIST

【讨论】:

    【解决方案4】:

    您可能会遇到问题,因为即使 Lisp 是按值传递对对象的引用,就像在 Java 或 Python 中一样。您的 cons 单元格包含您修改的引用,因此您修改原始的和本地的。

    IMO,您应该尝试以更实用的风格编写函数以避免此类问题。尽管 Common Lisp 是多范式,但函数式风格是更合适的方式。

    (defun 修改 (n) (cons 'x (cdr n))

    【讨论】:

    • 这是我的第一个 lisp 项目,我还不太了解函数式编程。我想我只需要重新考虑我构建程序的方式吗?我正在尝试在不修改状态的情况下检查对游戏状态的潜在修改。因此,我希望我可以修改状态的“副本”,而不是为 makechange 和 undochange 创建函数。
    • 通常避免不必要的状态更改是明智的。通常你不需要它们,它们会使你的程序更难调试并导致诸如此类的错误。
    猜你喜欢
    • 1970-01-01
    • 2021-06-15
    • 1970-01-01
    • 1970-01-01
    • 2019-04-24
    • 1970-01-01
    • 2021-12-02
    • 2021-03-05
    • 2020-09-13
    相关资源
    最近更新 更多