12.2 修改 (Modification)

为什么要避免共享结构呢?之前讨论的共享结构问题仅仅是个智力练习,到目前为止,并没使我们在实际写程序的时候有什么不同。当修改一个被共享的结构时,问题出现了。如果两个列表共享结构,当我们修改了其中一个,另外一个也会无意中被修改。

上一节中,我们介绍了怎样构建一个是其它列表的尾端的列表:

  1. (setf whole (list 'a 'b 'c)
  2. tail (cdr whole))

因为 wholecdrtail 是相等的,无论是修改 tail 还是 wholecdr ,我们修改的都是同一个 cons

  1. > (setf (second tail ) 'e)
  2. E
  3. > tail
  4. (B E)
  5. > whole
  6. (A B E)

同样的,如果两个列表共享同一个尾端,这种情况也会发生。

一次修改两个对象并不总是错误的。有时候这可能正是你想要的。但如果无意的修改了共享结构,将会引入一些非常微妙的 bug。Lisp 程序员要培养对共享结构的意识,并且在这类错误发生时能够立刻反应过来。当一个列表神秘的改变了的时候,很有可能是因为改变了其它与之共享结构的对象。

真正危险的不是共享结构,而是改变被共享的结构。为了安全起见,干脆避免对结构使用 setf (以及相关的运算,比如: poprplaca 等),这样就不会遇到问题了。如果某些时候不得不修改列表结构时,要搞清楚要修改的列表的来源,确保它不要和其它不需要改变的对象共享结构。如果它和其它不需要改变的对象共享了结构,或者不能预测它的来源,那么复制一个副本来进行改变。

当你调用别人写的函数的时候要加倍小心。除非你知道它内部的操作,否则,你传入的参数时要考虑到以下的情况:

1.它对你传入的参数可能会有破坏性的操作

2.你传入的参数可能被保存起来,如果你调用了一个函数,然后又修改了之前作为参数传入该函数的对象,那么你也就改变了函数已保存起来作为它用的对象[1]。

在这两种情况下,解决的方法是传入一个拷贝。

在 Common Lisp 中,一个函数调用在遍历列表结构 (比如, mapcarremove-if 的参数)的过程中不允许修改被遍历的结构。关于评估这样的代码的重要性并没有明确的规定。