Lisp的condition系统是其最伟大的特性之一。状况比异常更具一般性,状况可以代表程序执行过程中的任何事件。状况比异常更灵活,其将责任分为三部分:产生状况、处理以及再启动。
在多数语言中,错误处理方式为从一个失败的函数退出并返回给调用者,调用者根据情况采取后续的步骤:让自己也失败,或者忽略之,进一步地,尝试修复此错误。一些语言采用返回值的方式,另一些采用了异常抛出的方式。这两种方式存在一个共同的缺点:在错误出现后,调用栈将被回退,这样错误处理的代码无法在错误发生的现场进行工作。
1 CL的状况
CL提供了将错误恢复代码与如何恢复的代码分离的机制。当问题出现时,CL产生一个状况。
Define-condition宏用来定义了一个状况类,状况类默认继承自Condition基类。这个定义类的过程与defclass很象。但状况类的槽只能通过定义reader和accessor来读取。
新的状况对象使用make-condition来生成。
Error类是Condition类的子类,用于表示错误,如果状况是用于解决错误的,则应该继承自Error类。
(define-condition io-err (error)
((text :initarg: :errmsg :reader text)))
2 状况的生成与处理
2.1 抛出
在错误发生时,用户可以主动调用error函数来抛出一个状况
(类似于throw或raise)。error可以接收一个状况实例,也可以根据状况的名称和参数来生成一个实例。后者更常见:(error 'io-error :errmsg "...")
当一个状况发生时,如果没有建立相应的状况处理器,则会调用调试器。
2.2 捕获
使用handler-case来建立状况处理器
(与try-catch类似):
(handler-case expr
error-clause*)
每个error-clause的形式(codition-type ([var]) code)
其机制为:如果expr正常返回,则其值被handler-case返回;如果其中产生了状况,且其实例属于error-clause中指定的状况类型之一,则执行相应错误子句中的代码,且其返回值会被handler-case返回。形参var引用了状况对象,如果处理代码不需要,则可以省略。
3 再启动
如果只有状况处理器,那么状况与C类语言中的异常也没什么差别了,状况处理会导致调用栈返回。
而再启动才是状况的精髓之处,在底层不同的再启动中实现恢复错误的代码。而在中上层的状况处理器随后通过调用一个适当的再启动来选择不同的错误处理方式。
使用restart-case来建立再启动处理器:
(restart-case expr
restart-clause*)
每个restart-clause的形式(restart-name ([var]) code)
由于再启动并不直接处理expr抛出的状况,因而需要在上层建立绑定状况处理器来选择合适的再启动名称。
使用handler-bind来处理不同的再启动:
(handler-bind (binds*)
form*)
每个绑定的形式为(cond-type funcobj) funobj必须是只接受一个参数的函数对象,其所做的只是通过invoke-restart调用一个合适的再启动名字。
其机制为对于form*中抛出的状况进行捕获,并根据binds中提供了状况绑定列表选择合适的处理器,而处理器调用一个再启动,从而将调用栈再次返回到错误初始发生处。
funobj可以定义一个与再启动名称相同的函数,这样的函数称为再启动函数。更灵活的,可以使用find-restart来查找一个名称是否存在再启动对象,从而只在有再启动时才调用。从而使得控制流继续前进。
可以提供多人再启动,上层可以根据需要选择不同的错误处理恢复机制。CL定义了名为USE-VALUE的标准再启动,并为其定义了再启动函数
4 示例代码
(define-condition malformed-log-entry-error (error)
((test :initarg :text :reader text)))
(defun parse-log-entry (text)
(if (evenp text)
(list text "even")
(error 'malformed-log-entry-error :text text)))
(defun parse-log-file ()
(loop for i from 1 to 10
for entry = (restart-case (parse-log-entry i);;提供三个restart-case
(skip-log-entry () "a")
(use-value (v) (list v "odd"))
(reparse-entry (fixed-text) (parse-log-entry fixed-text)))
when entry collect it))
(defun analyze-log ()
(print (parse-log-file)))
(defun skip-log-entry (c)
(let ((restart (find-restart 'skip-log-entry)))
(when restart
(invoke-restart restart))))
(defun use-value-p (c)
(use-value (text c)))
(defun reparse-log-entry (c)
(let ((reparse (find-restart 'reparse-entry)))
(when reparse
(invoke-restart reparse (+ (text c) 11)))))
;;在高层函数中,根据不同的情况选择不同的再启动策略
(defun log-analyzer-skip ()
(handler-bind ((malformed-log-entry-error #'skip-log-entry))
(analyze-log)))
(defun log-analyzer-use ()
(handler-bind ((malformed-log-entry-error #'use-value-p))
(analyze-log)))
(defun log-analyzer-reparse ()
(handler-bind ((malformed-log-entry-error #'reparse-log-entry))
(analyze-log)))
5 状况的其他用法
理解状况的潜在用途的关键在于,抛出一个状况并不会改变程序的控制流;使用状况、状况处理和再启动,可以在底层和上层代码之间构建多种协议。
signal、warn和cerror函数提供了三个示例,无论底层代码需要何种方式与调用栈中的上层代码沟通信息,状况机制都可以合理利用。对于多数情况下,标准的warn和cerror协议已经足够。