有时候会出现这样的问题:在解释执行的函数里,宏调用每次被求值时都会重新展开;而在编译后的函数里,宏只会在编译阶段展开一次。如果宏定义本身带有副作用,执行效果就会因为展开次数不同而不一致。 因此,你应该避免在计算宏展开的过程中产生副作用,除非你非常清楚自己在做什么。
有一种特殊的 “副作用” 是无法避免的:构造 Lisp 对象。几乎所有宏展开都会构造列表,这也是大多数宏存在的意义。这通常是安全的,只有一种情况需要格外小心:当你构造的对象会成为宏展开式中被引用常量(quoted constant) 的一部分时。
如果宏只在编译时被展开一次,那么对应的对象也只会在编译期间被构造一次。但在解释执行时,宏每次被调用都会重新展开,这意味着每次都会构造一个新对象。
在大多数规范的 Lisp 代码中,这种差异并不会产生影响。只有当你对宏构造出来的对象执行副作用操作时,才会引发问题。因此,为避免麻烦,请避免对宏定义所构造的对象施加副作用。下面这个例子展示了这类副作用是如何引发问题的:
(defmacro empty-object () (list 'quote (cons nil nil)))
(defun initialize (condition)
(let ((object (empty-object)))
(if condition
(setcar object condition))
object))
如果 initialize 是解释执行的,那么每次调用它时,都会新建一个列表 (nil)。因此,多次调用之间不会保留副作用。如果 initialize 是编译执行的,那么宏 empty-object 会在编译时就展开,生成一个常量 (nil);之后每次调用 initialize,都会复用并修改这个同一个对象。
避免出现这类极端问题的一种思路是:把 empty-object 看成一种特殊常量,而不是用来分配内存的结构。你不会对 '(nil) 这样的常量使用 setcar,自然也不应该对 (empty-object) 使用它。