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
相关产品推荐
相关产品推荐

