setf
when 与 结合的行为let
让我感到困惑。在这里,setf
不会更改列表中的第二个值。
(defun to-temp-field (lst)
(let ((old-value (nth 2 lst))
(new-value 'hello))
(progn
(setf old-value new-value)
lst)))
但如果我不这样做let
,它会改变:
(defun to-temp-field (lst)
(let ((new-value 'hello))
(progn
(setf (nth 2 lst) new-value)
lst)))
是什么导致了这种行为?
Common Lisp 是最早展示共享调用的语言之一,这种范例现在在动态类型和高级编程语言中极为常见。在共享调用语言中,绑定到变量的值隐式地进行某种浅复制,采用最上面的指针或数据的新版本,但保留任何嵌套数据相同。
在 Lisp 中,这种区别实际上更容易看出。顶层 cons 单元会被复制,但任何嵌套指针都会被保留。
这里,
old-value
不是对列表中位置的引用。它是列表位置 2 处的值的浅表副本。正如卡奇克的回答所示,setf
实际上只是一个非常复杂的宏,宏根据它们所看到的语法来调度它们的行为。所以setf
它的第一个参数是old-value
。这是在编译时发生的,因此setf
无法知道它是old-value
来自列表还是来自调用nth
,因此它只是设置局部变量(基本上使用setq
)。现在,考虑你的第二个例子,
在这里,
setf
将(nth 2 lst)
其视为第一个参数。这不是局部变量。这实际上是一个类似函数调用的表达式。正如我们在hyperspec中看到的,nth
实际上有两个条目:一个用于正常函数调用,另一个用于在setf
. 那是因为,就像我们可以将普通函数定义为我们可以单独定义一个
setf
访问器为那不是伪代码。
defun
实际上接受(setf whatever)
作为有效的声明函数名称,并且当您setf
在函数调用上使用时,宏会查找具有该名称的函数。setf
接受一个位置和一个值,或多个位置/值对,并将位置的值更改为新值。第一个代码示例
old-value
是绑定在表单内的词法变量let
。词法变量是位置,因此调用将设置to(setf old-value new-value)
的值。old-value
new-value
函数形式也可以是地方,并且
nth
是一个可以指定地点的函数。在第二个代码示例中,调用(setf (nth 2 lst) new-value)
将 place 的值设置(nth 2 lst)
为 valuenew-value
。因此,在第一个示例中,更新了 place (
old-value
词法变量),而在第二个示例中,更新了place(nth 2 lst)
(列表的元素) 。lst
有几点需要注意:
第二个示例更新了 的第三个元素,
lst
因为列表在 Common Lisp 中是零索引的。由于表单的主体是隐式的,因此不需要
progn
内部的表单。let
let
progn
尝试修改文字会导致 Common Lisp 中出现未定义的行为,因此您不应该使用像这样的带引号列表来调用第二个函数:
(to-temp-field '(1 2 3))
或使用绑定到列表文字的变量,而是像这样:(to-temp-field (list 1 2 3))
或使用绑定到 a 的变量新构建的列表。setf
简而言之,它是一个宏,它了解正在设置的内容并根据该设置调用相应的集合,因此(setf old-value new value)
只需设置该值,而在第二种情况下,它的行为符合预期。setf
是一个宏,当第一个参数是符号时,它与列表结构的某些访问器执行完全不同的操作。很容易看出,当您提供符号时,它所做的事情与提供表单时完全不同
nth
:最后一行你看到它调用内部函数
%setnth
。这是特定于实现的,因此 SBCL 确实做了其他事情。然而; 这显然是在获取位置并执行与缓存 from to(rplaca (cddr lst) new-value)
值时相同的操作:(nth 2 lst)
old-value
这会执行一个操作
setq
,因此它只会重新绑定局部变量old-value
以指向99
。您无法缓存旧位置,因为宏将不再理解它。