You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Common Lisp传值与传引用问题:为何setf修改参数和修改car效果不同

Common Lisp 采用传值调用的参数传递规则,但这里传递的「值」是对象的引用,而非对象本身的深拷贝,这是两种操作表现差异的核心原因。


(setf lst NIL) 不生效的原因

调用(foo obj)时,形参lst会获得外层变量obj所持引用的拷贝,二者初始指向同一个列表对象,但属于两个独立的变量绑定。
(setf lst NIL)修改的是形参lst本身的绑定指向,仅把本地变量lst的指向改为NIL,既没有修改原列表对象的内容,也不会影响外层obj的绑定,因此外层访问obj时仍然指向原列表,看不到变化。

(defun foo (lst)
  ;; 仅修改本地lst的绑定,和外层obj无关联
  (setf lst NIL))

(setf (car lst) NIL) 生效的原因

(car lst)的语义是取出lst指向的列表对象的首元素槽位,setf作用于(car lst)时,会先通过lst持有的引用找到二者共同指向的原列表对象,直接修改该对象本身的首槽位值。
这个操作没有修改lst的绑定指向,修改的是两个引用共同指向的对象的内部内容,因此外层obj访问同一个对象时,就能看到修改后的结果。

(defun bar (lst)
  ;; 修改lst指向的列表对象的内部状态
  (setf (car lst) NIL))

你可以用便签类比理解这个逻辑:

  • 外层变量obj是一张写着列表存储地址的便签
  • 传参给lst相当于复印了一张地址相同的便签交给函数
  • setf lst NIL相当于把函数手里的复印件直接涂改掉,和外层的原便签完全无关
  • setf (car lst) NIL相当于你拿着复印件上的地址找到真实的列表对象,把对象的第一个元素改掉,不管用哪张便签找这个对象,看到的都是修改后的结果

如果你需要实现修改外层变量绑定的需求,有两种常用方案:

方案1:使用装箱(Box)包裹对象

把要修改的对象放到一个可变容器(比如cons)中传递,函数修改容器内部的指向即可:

;; 用cons作为装箱容器
(defun foo (box)
  (setf (car box) NIL))

(let ((obj (cons (list "a" "b") nil)))
  (foo obj)
  (car obj)) ; => NIL

方案2:使用宏操作外层绑定

宏可以直接操作传入的语法位置,不受函数传参规则限制:

(defmacro foo (place)
  `(setf ,place NIL))

(let ((obj (list "a" "b")))
  (foo obj)
  obj) ; => NIL

内容的提问来源于stack exchange,提问作者IgSokolov

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.28 11:06:03