在HANDLER-BIND
的HyperSpec条目中,它表示处理程序可以拒绝来处理信号。
然而,拒绝处理信号的链接词汇表条目并不是很有启发性:
拒绝v.(处理程序的)在没有处理正在发出信号的条件的情况下正常返回,允许信号处理过程继续,就像处理程序不存在一样。
此定义引出并没有回答什么构成不正常返回的问题?
是否有构成";处理";信号?
根据实践经验,我知道INVOKE-RESTART
似乎符合这一标准。但这是处理程序";手柄;一个信号,还是还有其他信号?
我认为要理解"handle"的真正含义,你应该考虑条件系统是如何在引擎盖下工作的。Kent-Pitman在标准化过程中编写的示例实现是一个很好的起点(尽管它有一些笨拙的东西,比如基本上实现整个对象系统,因为CLOS还不是语言的一部分)。
粗略地说,handler-bind
的作用是设置一个特殊的变量,我们将其称为*handler-clusters*
,以便它保存(type . function)
对的列表列表,与绑定列表相对应。一个可能的定义是
(defmacro handler-bind (bindings &body forms)
`(let ((*handler-clusters* (cons (list (mapcar #'(lambda (x) `(cons ',(car x) ,(cadr x)))
bindings))
*handler-clusters*)))
,@forms))
然后,signal
函数通过集群;如果它为正确的条件类型找到一个,它就会调用相关的函数。定义:
(defun signal (datum &rest arguments)
(let ((condition (coerce-to-condition datum arguments :default 'simple-condition))
(*handler-clusters* *handler-clusters*)) ; save current value
(when (typep condition *break-on-signals*)
(with-simple-restart (continue "Continue the signaling process")
(break "Break caused by *BREAK-ON-SIGNALS*")))
(loop for cluster := (pop *handler-clusters*) do
(loop for binding in cluster do
(when (typep condition (car binding))
(funcall (cdr binding) condition)))))
nil)
其中coerce-to-condition
是处理"条件指示符"的函数。一个微妙的点是,我们不能简单地(loop for cluster in *handler-clusters* do …)
,因为如果在调用处理程序的过程中,发出与正在处理的条件相同类型的条件的信号,那么处理程序将被递归调用,这可能是不可取的。因此,之前的值被保存,我们破坏性地pop
集群关闭。
现在,请记住Common Lisp允许对block
名称和tagbody
标记进行闭包。也就是说,在定义之后
(defvar *transfer-control*)
(defun weird (function)
(tagbody
(go :start)
:tag
(print 'transferred)
(return-from weird)
:start
(setf *transfer-control* (lambda () ; captures the tag :tag
(go :tag)))
(funcall function)))
类似的形式
(weird (lambda () (funcall *transfer-control*)))
允许,并将打印'transferred
。从某种意义上说,控制转移发生在tagbody
的词汇范围之外;CCD_ 14的能力已经"逃脱"了它的封闭范围。(由于tagbody
的动态范围已退出,所以在weird
返回后调用*transfer-control*
将是一个错误。)
所有这些都意味着,调用一个普通的Common Lisp函数可以导致控制权的转移,而不是返回一个值。调用*transfer-control*
只会将动态环境展开到tagbody
,然后跳到:tag
。函数不会按照通常的术语"返回",因为它所嵌入的表达式的求值将突然停止,永远不会恢复。(对于weird
和*transfer-control*
,我们定义了catch
和throw
的基元替换,它只传递控制权,但不同时传递值。要查看tagbody
、block
和catch
的相互定义,请参阅Henry Baker的"Common Lisp Special Forms的元循环语义"。)
因此,当signal
调用条件的处理程序时,可能会发生两件事:
- 处理程序转移控制,中止对
signal
的评估,并将堆栈展开到转移控制的位置 - 处理程序不传递控制权,但返回一个值。在这种情况下,正如上面的定义所示,
signal
将继续寻找一个处理程序,直到到达*handler-clusters*
的末尾,或者直到另一个处理进程转移控制权这被称为"衰退">
(在某种程度上,它也可以两者都不做,也可以两者兼而有之,例如,在另一个条件下调用signal
。规范称之为延迟。)
例如,超规范给出了handler-case
的样本展开。表单
(handler-case form
(type1 (var1) . body1)
(type2 (var2) . body2) ...)
成为(忽略变量捕获问题)
(block return-point
(let ((condition nil))
(tagbody
(handler-bind ((type1 #'(lambda (temp)
(setq condition temp)
(go :handler-tag-1)))
(type2 #'(lambda (temp)
(setq condition temp)
(go :handler-tag-2)))
...)
(return-from return-point form))
:handler-tag-1
(return-from return-point (let ((var1 condition)) . body1))
:handler-tag-2
(return-from return-point (let ((var2 condition)) . body2))
...)))
(我已经重写了hyperspec的代码,使其可读性更强,尽管不卫生,而且我还修复了原始代码中的一个错误。)
正如您所看到的,handler-case
建立的处理程序在被调用时无条件地传输控制权。因此,handler-case
处理程序肯定能够"处理"条件。
重新启动是以非常相似的方式实现的,restart-bind
设置动态环境,invoke-restart
使用它来调用函数。因为重新启动只是函数,它们不需要转移控制权,因此调用invoke-restart
并不总是一种"处理"行为,尽管如果重新启动是由restart-case
或with-simple-restart
建立的,当然,如果重新启动转移了控制权。
词汇表准确而简洁地描述了一些事情:如果处理程序正常返回,它就拒绝处理该条件。为了处理该条件,它必须转移控制权,这样它就永远不会返回。在CL中,这意味着它必须"向上"转移控制权。
就是一个例子
(block done
(handler-bind ((error (lambda (c)
(return-from done c))))
(error "exploded")))
这里,类型为error
的条件的处理程序正在处理该条件,因为它从不正常返回,而是从done
块返回。
这方面的完整描述在这里。
对任何缩进/paren错误表示歉意:我的lisp机器冒烟了,所以我在一个更原始的系统上键入它。