Common Lisp重启控制流疑惑:递归调用为何引发重复输出?
Common Lisp中Condition/Restart机制的执行流困惑:为何
After low.会打印两次? 重阅《Practical Common Lisp》第19章后,测试Common Lisp的condition与restart机制时遇到以下困惑:
以下代码中,重启会递归调用触发条件的low函数并传入新参数:
(define-condition no-fun-allowed-condition nil nil) (defun low (value) (format t "~&Before low.") (if (equal value "fun") (restart-case (signal 'no-fun-allowed-condition) (some-restart () (low "no-fun"))) (format t "~&Running low with a non-fun value: '~A'" value)) (format t "~&After low.")) (defun mid () (format t "~&Before mid.") (low "fun") (format t "~&After mid.")) (defun high () (handler-bind ((no-fun-allowed-condition #'(lambda (c) (declare (ignorable c)) (invoke-restart 'some-restart)))) (mid))) (high)
运行后输出:
Before mid. Before low. Before low. Running low with a non-fun value: 'no-fun' After low. After low. After mid.
Before low因递归打印两次可以理解,但After low也打印两次令我不解。推测是handler-bind不会unwind调用栈,递归调用的low执行完后,原low函数会继续执行,但原本以为重启会拦截控制流。这是否是递归调用加副作用导致的特殊情况?
解答
这不是特殊情况,完全符合Common Lisp Condition/Restart机制的设计逻辑,核心原因在于以下两点:
signal不会中断当前执行流:signal的作用只是通知已注册的条件处理器,它本身不会主动unwind调用栈,也不会终止当前函数的执行。handler-bind不截断调用栈:handler-bind注册的处理器在触发后,执行完重启动作(这里是递归调用low "no-fun"),程序会回到触发signal的位置,继续执行原函数中剩余的代码。
具体执行流程拆解:
- 调用
high→ 进入handler-bind上下文 → 调用mid,打印Before mid. - 调用
low "fun",打印Before low.→ 进入if分支,执行restart-case (signal ...) signal触发no-fun-allowed-condition,handler-bind中的处理器调用invoke-restart 'some-restart- 重启动作执行:递归调用
low "no-fun"→ 打印Before low.→ 进入else分支,打印运行信息 → 执行After low.,递归的low返回 - 回到最初的
low "fun"中的restart-case表达式,该表达式的返回值是重启动作的结果 → 继续执行low函数中剩余的(format t "~&After low."),因此第二次打印After low. - 最初的
low返回 →mid打印After mid.,程序结束
如果希望原low函数在触发重启后不再执行后续代码,可以通过以下方式调整:
- 用
error代替signal:error会主动触发栈unwind,配合restart-case时,重启动作执行后原函数流会被截断 - 在重启动作中使用
return-from显式跳出原low函数:比如把重启定义改为(some-restart () (return-from low (low "no-fun"))) - 改用
handler-case代替handler-bind:handler-case会在处理条件后unwind栈,终止原执行流
内容的提问来源于stack exchange,提问作者lupl
相关产品推荐
相关产品推荐

