catch 和 throw ¶大多数控制结构仅影响自身内部的控制流。而函数 throw 是常规程序执行规则中的一个例外:它可以根据请求执行非局部退出。(也存在其他例外,但它们仅用于错误处理。)throw 必须在 catch 内部使用,并会跳转回对应的 catch 处。例如:
(defun foo-outer ()
(catch 'foo
(foo-inner)))
(defun foo-inner ()
...
(if x
(throw 'foo t))
...)
若 throw 结构被执行,它会将控制流直接转移回对应的 catch 处,该 catch 会立即返回。throw 之后的代码不会被执行。throw 的第二个参数会被用作 catch 的返回值。
函数 throw 会根据第一个参数查找匹配的 catch:它会搜索第一个参数与 throw 中指定参数满足 eq 相等性的 catch。若存在多个符合条件的 catch,则最内层的那个优先生效。因此,在上述示例中,throw 指定了 foo,而 foo-outer 中的 catch 也指定了同一个符号,所以该 catch 就是匹配的目标(假设两者之间没有其他匹配的 catch)。
执行 throw 会退出直至匹配 catch 为止的所有 Lisp 结构,包括函数调用。当 let 等绑定结构或函数调用以这种方式退出时,其建立的绑定会被解除 ——这与这些结构正常退出时的行为完全一致(see 局部变量)。同理,throw 会恢复由 save-excursion 保存的缓冲区和光标位置(see Excursions),以及由 save-restriction 保存的缓冲区缩窄状态。此外,当 throw 退出 unwind-protect 特殊形式时,还会执行该形式所设置的所有清理操作(see 非局部退出的清理工作)。
throw 并不需要在词法上出现在它所跳转的 catch 内部。它同样可以从 catch 内部调用的另一个函数中被执行。只要 throw 的执行时机,在时间顺序上处于进入 catch 之后、退出 catch 之前,它就能够找到并跳转到该 catch。这也是为什么 throw 可以用在诸如 exit-recursive-edit 这类命令中,用于跳转回编辑器命令循环(see Recursive Editing)。
Common Lisp 注意: 大多数其他 Lisp 方言(包括 Common Lisp)提供了多种非顺序控制转移的方式,例如
return、return-from和go。而 Emacs Lisp 只提供了throw。cl-lib 库提供了其中一部分的实现。See Blocks and Exits in Common Lisp Extensions。
catch 会为 throw 函数建立一个返回点。该返回点通过 tag(标记)与其他返回点区分开,tag 可以是除 nil 之外的任意 Lisp 对象。在建立返回点之前,参数 tag 会先被正常求值。
当返回点生效后,catch 会按文本顺序对 body(代码体)中的形式依次求值。如果这些形式正常执行(无错误、无非局部退出),则 catch 会返回 body 中最后一个形式的求值结果。
若在 body 执行期间触发了 throw,且该 throw 指定的标记值与 tag 相同,则 catch 形式会立即退出;其返回值为 throw 的第二个参数所指定的值。
throw 的作用是从先前通过 catch 建立的返回点中返回。参数 tag(标记)用于从多个已存在的返回点中选择匹配的目标;它必须与 catch 中指定的标记值满足 eq 相等性。若有多个返回点匹配该 tag,则使用最内层的那个返回点。
参数 value 会被用作对应 catch 的返回值。
如果当前不存在标记为 tag 且生效的返回点,Emacs 会触发 no-catch 错误,并将 (tag value) 作为该错误的附加数据。