12.2 修改 (Modification)
为什么要避免共享结构呢?之前讨论的共享结构问题仅仅是个智力练习,到目前为止,并没使我们在实际写程序的时候有什么不同。当修改一个被共享的结构时,问题出现了。如果两个列表共享结构,当我们修改了其中一个,另外一个也会无意中被修改。
上一节中,我们介绍了怎样构建一个是其它列表的尾端的列表:
(setf whole (list 'a 'b 'c)
tail (cdr whole))
因为 whole
的 cdr
与 tail
是相等的,无论是修改 tail
还是 whole
的 cdr
,我们修改的都是同一个 cons
:
> (setf (second tail ) 'e)
E
> tail
(B E)
> whole
(A B E)
同样的,如果两个列表共享同一个尾端,这种情况也会发生。
一次修改两个对象并不总是错误的。有时候这可能正是你想要的。但如果无意的修改了共享结构,将会引入一些非常微妙的 bug。Lisp 程序员要培养对共享结构的意识,并且在这类错误发生时能够立刻反应过来。当一个列表神秘的改变了的时候,很有可能是因为改变了其它与之共享结构的对象。
真正危险的不是共享结构,而是改变被共享的结构。为了安全起见,干脆避免对结构使用 setf
(以及相关的运算,比如: pop
, rplaca
等),这样就不会遇到问题了。如果某些时候不得不修改列表结构时,要搞清楚要修改的列表的来源,确保它不要和其它不需要改变的对象共享结构。如果它和其它不需要改变的对象共享了结构,或者不能预测它的来源,那么复制一个副本来进行改变。
当你调用别人写的函数的时候要加倍小心。除非你知道它内部的操作,否则,你传入的参数时要考虑到以下的情况:
1.它对你传入的参数可能会有破坏性的操作
2.你传入的参数可能被保存起来,如果你调用了一个函数,然后又修改了之前作为参数传入该函数的对象,那么你也就改变了函数已保存起来作为它用的对象[1]。
在这两种情况下,解决的方法是传入一个拷贝。
在 Common Lisp 中,一个函数调用在遍历列表结构 (比如, mapcar
或 remove-if
的参数)的过程中不允许修改被遍历的结构。关于评估这样的代码的重要性并没有明确的规定。