LISP - 错误处理
Lisp - 现代错误处理
Section titled “Lisp - 现代错误处理”错误处理是编写健壮软件的关键部分。Common Lisp 具有独特而强大灵活的条件系统 (Condition System)。与许多语言中仅将调用栈展开到处理程序以处理异常不同,Lisp 条件系统允许将异常情况的发出与如何处理它分离。这使得能够创建高弹性、交互式程序。
该系统建立在三个核心概念之上:
- 发出条件信号:通知系统发生了异常事件。这可以是致命错误、警告,或仅仅是通知。
- 处理条件:捕获发出信号的条件并决定如何处理的代码。
- 重启 (Restarting):一个独特的特性,发出条件信号的代码可以提供恢复策略。处理程序可以选择其中一种策略来恢复执行,而无需完全展开堆栈。
简单错误处理:handler-case
Section titled “简单错误处理:handler-case”对于许多情况,handler-case 提供了简单而熟悉的 try-catch 风格的错误处理。它执行一个代码块,如果指定类型的条件被发出信号,它会中止该代码块并运行相应的处理程序代码。
示例:健壮的除法
Section titled “示例:健壮的除法”让我们创建一个安全地除以两个数字的函数,优雅地处理内置的 division-by-zero 条件。
;;; main.lisp
(defun safe-divide (numerator denominator) "除以两个数字,返回结果或错误时的描述性消息。" (handler-case ;; 尝试的代码 (/ numerator denominator)
;; 错误子句:处理类型为 'division-by-zero' 的条件 (division-by-zero (c) (format nil "Error: Cannot divide by zero. (Details: ~a)" c))))
;; --- 测试用例 ---(format t "10 / 2 = ~a~%" (safe-divide 10 2))(format t "10 / 0 = ~a~%" (safe-divide 10 0))(format t "-5 / 2 = ~a~%" (safe-divide -5 2))10 / 2 = 510 / 0 = Error: Cannot divide by zero. (Details: Division by zero.)-5 / 2 = -5/2高级交互式处理:重启 (Restarts)
Section titled “高级交互式处理:重启 (Restarts)”条件系统的真正强大之处在于重启 (restarts)。重启允许发出错误的低级函数提供恢复选项。然后,高级处理程序可以选择其中一个选项来恢复执行,通常低级函数甚至不知道它曾被暂停。
让我们构建一个更复杂的除法函数来演示这一点。
步骤 1:定义自定义条件
Section titled “步骤 1:定义自定义条件”首先,我们使用 define-condition 定义我们自己的特定条件类型。这使我们的处理程序更加精确。
;; 为我们的应用程序定义一个特定条件(define-condition invalid-divisor (error) ((value :initarg :value :reader invalid-divisor-value)) (:report (lambda (condition stream) (format stream "The value ~s is an invalid divisor." (invalid-divisor-value condition)))))步骤 2:发出条件信号并提供重启选项
Section titled “步骤 2:发出条件信号并提供重启选项”如果分母为零,我们的除法函数将发出自定义条件信号。关键是,它使用 restart-case 提供两种恢复策略:一种是使用不同的值,另一种是只返回 NIL。
;; 发出错误信号并提供解决方案的低级函数(defun recoverable-divide (numerator denominator) (if (zerop denominator) ;; 如果分母为零,则发出我们的条件信号。 (restart-case (error 'invalid-divisor :value denominator) ;; 重启 1:允许调用者提供一个新的分母。 (use-new-denominator (new-denominator) :report "Specify a new denominator to use for the division." (recoverable-divide numerator new-denominator))
;; 重启 2:允许调用者简单地中止并返回 NIL。 (abort-division () :report "Abort the division and return NIL." nil))
;; 如果一切正常,则执行除法。 (/ numerator denominator)))步骤 3:处理条件并调用重启
Section titled “步骤 3:处理条件并调用重启”现在,一个更高级别的函数可以调用 recoverable-divide 并使用 handler-bind 来处理错误。与 handler-case 不同,handler-bind 不会展开堆栈。它可以检查条件,然后使用 invoke-restart 选择一个可用的恢复选项。
;; 以交互方式处理错误的高级函数。(defun division-ui () (handler-bind ((invalid-divisor (lambda (c) (declare (ignore c)) ; 我们在这里不需要条件对象 (format *error-output* "~&Error signaled! Attempted to divide by zero.~%") (format *error-output* "Let's try to recover by providing a new denominator.~%") ;; 选择 'use-new-denominator' 重启并传递 '2' 作为新值。 (invoke-restart 'use-new-denominator 2)))) (let ((result (recoverable-divide 10 0))) (format t "~&The final result of the division is: ~a~%" result))))
(division-ui)Error signaled! Attempted to divide by zero.Let's try to recover by providing a new denominator.The final result of the division is: 5常用信号函数
Section titled “常用信号函数”以下是用于发出条件信号的标准函数摘要。
| 函数 | 描述和用例 |
|---|---|
| error datum &rest args | 发出一个致命的、不可继续的错误信号。最常见的表示出现问题且在不干预的情况下无法正常执行的方式。 |
| cerror continue-format-string error-datum &rest args | 发出一个可继续的错误信号。如果这在交互式调试器(如 SLIME)中被发出信号,用户将看到一个选项来继续执行。这在开发过程中非常有用。 |
| warn datum &rest args | 发出一个非致命警告信号。警告打印后程序执行继续。用于提醒用户潜在问题,例如使用了已废弃函数。 |
| break &optional format-string &rest args | 无条件进入调试器。这是一个强大的调试工具,行为类似于断点,也允许交互式检查和修改程序状态。 |
最佳实践和总结
Section titled “最佳实践和总结”- 使用
handler-case处理简单的try-catch逻辑,即您只想处理错误并继续执行。 - 为您的应用程序定义自定义条件,以使处理程序更具体,并使您的代码更具自文档化能力。
- 使用
restart-case和handler-bind来构建弹性系统,其中低级代码可以提供恢复策略,而高级代码可以决定如何使用它们。 - 在开发过程中利用
cerror在满足某些条件时创建可继续的断点。