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"。
第六步:避免常见陷阱
-
不要遗漏错误类型:如果错误类型写错(如拼写错误),
handler-case会忽略它,导致程序崩溃。- ✅ 正确:
(division-by-zero ...) - ❌ 错误:
(division-by-zer0 ...)(数字 0 代替字母 o)
- ✅ 正确:
-
变量名必须存在:即使不用错误对象,也需声明形参。
- ✅ 正确:
(division-by-zero (c) ...) - ❌ 错误:
(division-by-zero () ...)(某些实现会报错)
- ✅ 正确:
-
不要混淆 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))
暂无评论,快来抢沙发吧!