当你临时将数据结构置于不一致状态时,unwind-protect 结构是必不可少的;它能确保在发生错误或 throw 时,将数据恢复到一致状态。(另一个仅用于缓冲区内容修改的专用清理结构是原子修改组;Atomic Change Groups。)
unwind-protect 执行 body-form,并保证:无论以何种方式退出 body-form,都会执行 cleanup-forms。body-form 可能正常结束、通过 throw 跳出 unwind-protect,或触发错误;在所有情况下,cleanup-forms 都会被执行。
如果 body-form 正常结束,unwind-protect 会在执行完 cleanup-forms 后,返回 body-form 的值。如果 body-form 未正常结束,unwind-protect 就不会按正常方式返回任何值。
只有 body-form 受到 unwind-protect 的保护。如果某个 cleanup-forms 自身发生了非局部退出(通过 throw 或错误),unwind-protect 并 不保证 会执行剩下的清理代码。
如果某个 cleanup-forms 执行失败可能导致问题,可以在该表达式外层再用一个 unwind-protect 对其进行保护。
例如,下面我们创建一个不可见的缓冲区用于临时操作,并确保在结束前将其杀死:
(let ((buffer (get-buffer-create " *temp*")))
(with-current-buffer buffer
(unwind-protect
body-form
(kill-buffer buffer))))
你可能会觉得,我们完全可以写成 (kill-buffer (current-buffer)),从而省去变量 buffer。但上面这种写法更安全,可以避免 body-form 在切换到其他缓冲区后发生错误的情况!(另一种方案是在 body-form 外层使用 save-current-buffer,确保临时缓冲区能重新成为当前缓冲区,以便正常关闭。)
Emacs 提供了一个名为 with-temp-buffer 的标准宏,它展开后的代码大致就和上面展示的一样(see Current Buffer)。本手册中定义的多个宏都以这种方式使用了 unwind-protect。
下面是一个来自某 FTP 包的真实示例。它会创建一个进程(see Processes),尝试与远程机器建立连接。由于 ftp-login 函数很容易遇到各种编写者无法预料的问题,因此用一个特殊形式对其加以保护,确保在出错时能删除该进程。否则,Emacs 中可能会堆积大量无用的子进程。
(let ((win nil))
(unwind-protect
(progn
(setq process (ftp-setup-buffer host file))
(if (setq win (ftp-login process host user password))
(message "Logged in")
(error "Ftp login failed")))
(or win (and process (delete-process process)))))
这个示例存在一个小 bug:如果用户按下 C-g 退出,且退出恰好发生在函数 ftp-setup-buffer 返回之后、变量 process 赋值之前,那么该进程将不会被销毁。修复这个 bug 没有简单的办法,但至少这种情况发生的概率极低。