接上一节,先执行如下代码,观看效果:
(do-primes-3-1 (ending-value 0 10) (format t "~d~t" ending-value))
,死机没?
最后一个需要堵上的漏洞是由于使用了变量名ending-value而产生的。
问题在于这个名字(其应当完全属于宏实现内部的细节)它可以跟传递给宏的代码或是宏被调用的上下文产生交互。
上述代码将无限循环下去,由于ending-value永远不会大于其余自身。
为了补上这一漏洞,需要一个永远不会在宏展开代码之外被用到的符号,你可以深度使用一个真正罕见的名字
但即便如此也无法做到万无一失
函数gensym在其每次被调用时返回唯一的符号。
这是一个没有被lisp读取器读过的符号并且永远不会被读到,因为它不会进入到任何包里
因而就可以在每次执行被展开时生成一个新的符号以替代ending-value这样的字面名称
;;;修复ending-value调用名漏洞
(defmacro do-primes-3-2 ( (var start end) &body body)
(let ((ending-value (gensym)))
`(do ((,var(next-prime ,start) (next-prime(1+ ,var)))
(,ending-value ,end))
((> ,var ,ending-value))
,@body)))
生成展开式
(DO ((ENDING-VALUE (NEXT-PRIME 0) (NEXT-PRIME (1+ ENDING-VALUE)))
(#:G251 19))
((> ENDING-VALUE #:G251))
(FORMAT T "~a~t" ENDING-VALUE))
现在用来保存循环终值的变量的生成符号, #:G251
该符号名字由gensym函数生成,带有前缀 #:
注:当ending-value为展开式中的变量时,需要在代码中ending-value前加 逗号
在宏编写中只须遵循下面所概括的这些规则使你可以将许多漏洞预先被堵上:
1.除非有特殊理由,否则需要将展开式中的任何子形式放在一个位置上,使其求值顺序与宏调用的子形式相同。
2.除非有特殊理由,否则需要确保子形式仅被求值一次,方法是在展开式中创建变量来持有求值参数形式所得到的值,然后在展开式中所有需要用到该值的地方使用这个变量。
3.在宏展开期使用gensym来创建展开式中用到的变量名。