14.5.2 宏参数的重复求值问题

定义宏时,必须留意参数在展开代码执行时会被求值多少次。下面这个用于简化循环的宏示例,就展示了这类问题。该宏可以让我们写出类似 for 循环的结构。

(defmacro for (var from init to final do &rest body)
  "Execute a simple \"for\" loop.
For example, (for i from 1 to 10 do (print i))."
  (list 'let (list (list var init))
        (cons 'while
              (cons (list '<= var final)
                    (append body (list (list 'inc var)))))))

(for i from 1 to 3 do
   (setq square (* i i))
   (princ (format "\n%d %d" i square)))
→
(let ((i 1))
  (while (<= i 3)
    (setq square (* i i))
    (princ (format "\n%d %d" i square))
    (inc i)))

     ⊣1       1
     ⊣2       4
     ⊣3       9
⇒ nil

该宏中的 fromtodo 参数仅作为语法糖(syntactic sugar) 存在 —— 它们会被完全忽略。设计思路是让你在宏调用的这些位置写入这类 “无实际作用的辅助词”(例如 fromtodo),仅用于提升代码可读性。

以下是使用反引号简化后的等效定义:

(defmacro for (var from init to final do &rest body)
  "Execute a simple \"for\" loop.
For example, (for i from 1 to 10 do (print i))."
  `(let ((,var ,init))
     (while (<= ,var ,final)
       ,@body
       (inc ,var))))

该定义的两种写法(使用反引号和不使用反引号)都存在一个缺陷:final 会在每次循环迭代时都被求值。若 final 是常量,这不会有问题;但如果它是更复杂的表达式(例如 (long-complex-calculation x)),则会显著降低执行速度;若 final 带有副作用,多次执行它很可能导致逻辑错误。

一个设计良好的宏定义会主动规避这类问题:生成的展开式会确保参数表达式仅被求值一次(除非重复求值是该宏的设计意图)。以下是 for 宏的正确展开版本:

(let ((i 1)
      (max 3))
  (while (<= i max)
    (setq square (* i i))
    (princ (format "%d      %d" i square))
    (inc i)))

以下是能生成该展开式的宏定义:

(defmacro for (var from init to final do &rest body)
  "Execute a simple for loop: (for i from 1 to 10 do (print i))."
  `(let ((,var ,init)
         (max ,final))
     (while (<= ,var max)
       ,@body
       (inc ,var))))

遗憾的是,这一修复方案引入了另一个问题,具体将在下一节说明。


emacs

Emacs

org-mode

Orgmode

Donations

打赏

Copyright

© 2025 Jasper Hsu

Creative Commons

Creative Commons

Attribute

Attribute

Noncommercial

Noncommercial

Share Alike

Share Alike