文章目录

Lisp 异常处理:handler-case

发布于 2026-04-02 07:02:19 · 浏览 9 次 · 评论 0 条

Lisp 异常处理:handler-case

在 Common Lisp 中,程序运行时可能遇到各种意外情况,比如除以零、访问不存在的数组下标,或文件无法打开。handler-case 是处理这类异常(也叫“条件”)的核心工具,它让你能优雅地捕获错误并指定应对措施,而不是让程序直接崩溃。


什么是 handler-case?

handler-case 是一个宏,用于包裹一段可能出错的代码,并为特定类型的错误定义处理逻辑。它的基本结构如下:

(handler-case 表达式
  (错误类型1 (变量) 处理代码1)
  (错误类型2 (变量) 处理代码2)
  ...)
  • 表达式 是你希望执行但可能抛出错误的代码。
  • 每个 (错误类型 (变量) ...) 子句定义了如何处理某一类错误。
  • 如果 表达式 正常执行,handler-case 直接返回其结果;如果抛出错误且有匹配的处理子句,则执行对应处理代码并返回其结果。

第一步:识别常见错误类型

Common Lisp 的错误都继承自 condition 类型,其中最常用的是 error。以下是几个典型错误类型:

错误类型 触发场景 示例
division-by-zero 除以零 (/ 1 0)
type-error 类型不匹配 (car 42)
file-error 文件操作失败 (open "nonexistent.txt")
simple-error 通用错误 (error "Oops!")

要使用 handler-case,你需要知道可能抛出的具体错误类型。


第二步:编写基础 handler-case 表达式

编写一个能安全执行除法的函数:

(defun safe-divide (a b)
  (handler-case (/ a b)
    (division-by-zero (c)
      (declare (ignore c))
      :infinity)))
  • 调用 (safe-divide 10 2) 返回 5
  • 调用 (safe-divide 10 0) 返回 :infinity,而不是崩溃。
  • (declare (ignore c)) 告诉编译器忽略错误对象 c,避免警告(因为这里没用到它)。

第三步:处理多个错误类型

扩展上面的函数,同时处理类型错误和除零错误:

(defun robust-divide (a b)
  (handler-case (/ a b)
    (division-by-zero (c)
      (declare (ignore c))
      "Cannot divide by zero")
    (type-error (c)
      (format nil "Invalid types: ~A" c))))
  • 如果传入非数字(如 (robust-divide "a" "b")),会触发 type-error,返回描述性字符串。
  • handler-case 按顺序检查子句,第一个匹配的会被执行。

第四步:访问错误详情

错误对象(如上面的 c)包含有用信息。使用 format 或访问槽位来提取细节。

例如,捕获文件错误并打印原因:

(defun read-file-safe (filename)
  (handler-case (with-open-file (s filename)
                  (read s))
    (file-error (c)
      (format nil "Failed to open ~A: ~A"
              filename
              (file-error-pathname c)))))
  • file-error-pathname 是专门用于提取文件路径的函数。
  • 对于通用错误,可用 (format nil "~A" c) 获取人类可读描述。

第五步:嵌套与作用域规则

handler-case 的处理子句只捕获其内部表达式直接或间接抛出的错误。错误不会“穿透”到外层,除非内层没有匹配处理。

观察以下嵌套行为:

(handler-case
    (handler-case (/ 1 0)
      (arithmetic-error (c) 
        (declare (ignore c))
        (error "Wrapped error"))) ; 抛出新错误
  (simple-error (c)
    (format nil "Caught: ~A" c)))
  • 内层捕获 division-by-zero(它是 arithmetic-error 的子类),然后主动抛出一个 simple-error
  • 外层成功捕获这个新错误,最终返回 "Caught: Wrapped error"

第六步:避免常见陷阱

  1. 不要遗漏错误类型:如果错误类型写错(如拼写错误),handler-case 会忽略它,导致程序崩溃。

    • ✅ 正确:(division-by-zero ...)
    • ❌ 错误:(division-by-zer0 ...)(数字 0 代替字母 o)
  2. 变量名必须存在:即使不用错误对象,也需声明形参。

    • ✅ 正确:(division-by-zero (c) ...)
    • ❌ 错误:(division-by-zero () ...)(某些实现会报错)
  3. 不要混淆 condition 和 exception:Lisp 的“条件系统”比传统异常更灵活,支持“继续”等操作,但 handler-case 仅用于处理不可恢复的错误(即 error 类型)。对于可恢复的条件(如 warn),应使用 handler-bind


实战:构建一个安全的配置加载器

创建一个函数,尝试从文件加载配置,失败时返回默认值:

(defun load-config-or-default (filename &optional default)
  (handler-case
      (with-open-file (stream filename)
        (let ((config (read stream)))
          (if (and (listp config) (evenp (length config)))
              config
              (error "Invalid config format"))))
    (file-error (c)
      (format t "Warning: Config file ~A not found. Using defaults.~%"
              (file-error-pathname c))
      default)
    (error (c)
      (format t "Warning: Bad config in ~A: ~A. Using defaults.~%"
              filename c)
      default)))
  • 如果文件不存在,捕获 file-error,打印警告并返回默认值。
  • 如果文件内容格式错误(如不是偶数长度的列表),手动抛出 error,被第二个子句捕获。
  • 所有错误都被妥善处理,程序继续运行。

总结关键语法模式

(handler-case (risky-code arg1 arg2)
  (specific-error-type (condition-object)
    ;; 处理逻辑,可返回任意值
    fallback-value)
  (general-error (c)
    ;; 通用兜底处理
    (log-error c)
    nil))
  • 始终明确错误类型,避免过度宽泛(如直接用 error 而不区分具体子类)。
  • 处理代码应简洁,避免在错误处理中引入新错误。
  • 测试边界情况:传入非法参数、删除文件、模拟资源不足等。
;; 示例:完整测试用例
(handler-case (car 42)
  (type-error (c)
    (assert (string= "The value 42 is not of type LIST."
                     (format nil "~A" c)))
    'handled))

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文