LISP - 错误处理



在 Common LISP 术语中,异常被称为条件。

事实上,条件比传统编程语言中的异常更通用,因为条件表示任何可能影响函数调用栈各个级别的事件,无论是错误还是非错误。

LISP 中的条件处理机制以这样一种方式处理此类情况:条件用于发出警告(例如打印警告),而调用栈上的上层代码可以继续其工作。

LISP 中的条件处理系统包含三个部分:

  • 发出条件
  • 处理条件
  • 重新启动过程

处理条件

让我们以一个处理因除以零引起的条件的示例来解释这里概念。

处理条件需要执行以下步骤:

  • 定义条件 - “条件是一个对象,其类指示条件的总体性质,其实例数据携带有关导致发出条件的特定情况的详细信息”。

    define-condition 宏用于定义条件,其语法如下:

(define-condition condition-name (error)
   ((text :initarg :text :reader text))
)
  • 新的条件对象使用 MAKE-CONDITION 宏创建,它根据:initargs参数初始化新条件的槽。

在我们的示例中,以下代码定义了条件:

(define-condition on-division-by-zero (error)
   ((message :initarg :message :reader message))
)
  • 编写处理程序 - 条件处理程序是用于处理已发出的条件的代码。它通常编写在调用出错函数的高级函数之一中。当发出条件时,发出机制会根据条件的类搜索合适的处理程序。

    每个处理程序包含:

    • 类型说明符,指示它可以处理的条件类型
    • 一个函数,它接受一个参数:条件

    当发出条件时,发出机制会找到与条件类型兼容的最近建立的处理程序,并调用其函数。

    handler-case建立条件处理程序。handler-case的基本形式:

(handler-case expression error-clause*)

其中,每个错误子句都具有以下形式:

condition-type ([var]) code)
  • 重启阶段

    这是实际从错误中恢复程序的代码,然后条件处理程序可以通过调用适当的重启来处理条件。重启代码通常放置在中级或低级函数中,而条件处理程序则放置在应用程序的上层。

    handler-bind宏允许您提供重启函数,并允许您在不展开函数调用栈的情况下继续执行低级函数。换句话说,控制流仍然在低级函数中。

    handler-bind的基本形式如下:

(handler-bind (binding*) form*)

其中每个绑定都是以下列表:

  • 条件类型
  • 一个带有单个参数的处理程序函数

invoke-restart宏查找并调用最近绑定的重启函数,并使用指定的名称作为参数。

您可以有多个重启。

示例

在此示例中,我们通过编写一个名为 division-function 的函数来演示上述概念,如果除数参数为零,该函数将创建错误条件。我们有三个匿名函数,它们提供了三种退出方法:返回一个值 1,发送一个除数 2 并重新计算,或返回 1。

创建一个名为 main.lisp 的新源代码文件,并在其中键入以下代码。

(define-condition on-division-by-zero (error)
   ((message :initarg :message :reader message))
)
   
(defun handle-infinity ()
   (restart-case
      (let ((result 0))
         (setf result (division-function 10 0))
         (format t "Value: ~a~%" result)
      )
      (just-continue () nil)
   )
)
     
(defun division-function (value1 value2)
   (restart-case
      (if (/= value2 0)
         (/ value1 value2)
         (error 'on-division-by-zero :message "denominator is zero")
      )

      (return-zero () 0)
      (return-value (r) r)
      (recalc-using (d) (division-function value1 d))
   )
)

(defun high-level-code ()
   (handler-bind
      (
         (on-division-by-zero
            #'(lambda (c)
               (format t "error signaled: ~a~%" (message c))
               (invoke-restart 'return-zero)
            )
         )
         (handle-infinity)
      )
   )
)

(handler-bind
   (
      (on-division-by-zero
         #'(lambda (c)
            (format t "error signaled: ~a~%" (message c))
            (invoke-restart 'return-value 1)
         )
      )
   )
   (handle-infinity)
)

(handler-bind
   (
      (on-division-by-zero
         #'(lambda (c)
            (format t "error signaled: ~a~%" (message c))
            (invoke-restart 'recalc-using 2)
         )
      )
   )
   (handle-infinity)
)

(handler-bind
   (
      (on-division-by-zero
         #'(lambda (c)
            (format t "error signaled: ~a~%" (message c))
            (invoke-restart 'just-continue)
         )
      )
   )
   (handle-infinity)
)

(format t "Done."))

执行代码时,它将返回以下结果:

error signaled: denominator is zero
Value: 1
error signaled: denominator is zero
Value: 5
error signaled: denominator is zero
Done.

除了上面讨论的“条件系统”之外,Common LISP 还提供了各种可用于发出错误的函数。但是,发出错误后的错误处理方式取决于实现。

LISP 中的错误发出函数

下表提供了常用函数,用于发出警告、中断、非致命和致命错误。

用户程序指定错误消息(字符串)。这些函数处理此消息,可能会或可能不会将其显示给用户。

错误消息应通过应用format函数构建,开头和结尾都不应包含换行符,并且不需要指示错误,因为 LISP 系统将根据其首选样式处理这些错误。

序号 函数和描述
1

error format-string &rest args

它发出致命错误。无法从这种错误中继续;因此,error 永远不会返回到其调用者。

2

cerror continue-format-string error-format-string &rest args

它发出错误并进入调试器。但是,它允许程序在解决错误后从调试器中继续执行。

3

warn format-string &rest args

它打印错误消息,但通常不会进入调试器

4

break &optional format-string &rest args

它打印消息并直接进入调试器,不允许程序错误处理功能拦截。

示例

在此示例中,factorial 函数计算数字的阶乘;但是,如果参数为负数,则会引发错误条件。

创建一个名为 main.lisp 的新源代码文件,并在其中键入以下代码。

(defun factorial (x)
   (cond ((or (not (typep x 'integer)) (minusp x))
      (error "~S is a negative number." x))
      ((zerop x) 1)
      (t (* x (factorial (- x 1))))
   )
)

(write(factorial 5))
(terpri)
(write(factorial -1))

执行代码时,它将返回以下结果:

120
*** - -1 is a negative number.
广告