Clojure 异常处理:try、catch、finally
Clojure 通过 try 表达式提供结构化的异常处理机制,允许你捕获 Java 异常并执行清理逻辑。其语法简洁,与 Java 的 try-catch-finally 模型对应,但完全融入 Clojure 的函数式风格。
基本结构
使用 try 包裹可能抛出异常的代码,用 catch 指定要捕获的异常类型和处理方式,可选地用 finally 定义无论是否发生异常都必须执行的清理代码。
基本形式如下:
(try
; 可能抛出异常的表达式
(risky-operation)
(catch ExceptionType e
; 处理异常的代码
(handle-exception e))
(finally
; 清理代码
(cleanup)))
注意:
try必须至少包含一个catch或finally子句。catch子句可以有多个,按顺序匹配异常类型。finally最多只能有一个,且必须放在所有catch之后。
捕获异常
编写 catch 子句时,先声明要捕获的异常类(全限定名或已导入的简名),再指定绑定符号接收异常对象。
例如,捕获 java.io.IOException:
(try
(slurp "/nonexistent/file.txt")
(catch java.io.IOException e
(println "文件读取失败:" (.getMessage e))))
如果已通过 ns 导入 IOException,可简化为:
(ns my-app.core
(:import [java.io IOException]))
(try
(slurp "/bad/path")
(catch IOException e
(println "IO 错误:" (.getMessage e))))
添加多个 catch 子句以处理不同类型的异常,Clojure 会按顺序尝试匹配:
(try
(Integer/parseInt "not-a-number")
(catch NumberFormatException e
(println "不是有效数字"))
(catch RuntimeException e
(println "运行时错误:" (.getMessage e))))
注意:更具体的异常类型应放在更通用的类型之前,否则会被提前捕获而无法到达后续子句。
使用 finally 执行清理
将资源释放或状态重置等操作放入 finally 块中,确保它们总被执行。
典型场景是关闭文件流或网络连接:
(let [reader (java.io.BufferedReader. (java.io.StringReader. "data"))]
(try
(.readLine reader)
(catch Exception e
(println "读取时出错"))
(finally
(.close reader))))
即使 (.readLine reader) 抛出异常,(.close reader) 仍会执行。
重要:
finally中的代码不影响try表达式的返回值。整个try表达式的值由try主体、某个catch块或finally之前的最后一个表达式决定。不要在finally中返回值或抛出新异常,这会掩盖原始结果或异常。
返回值规则
理解 try 表达式的返回值来源至关重要:
- 如果
try主体正常完成,其最后一个表达式的值就是整个try的返回值。 - 如果发生异常且被某个
catch捕获,该catch块的最后一个表达式的值成为返回值。 finally块的值永远被忽略,仅用于副作用。
示例:
(def result
(try
(+ 1 2) ; 返回 3
(catch Exception e
"error")))
; result => 3
(def result2
(try
(/ 1 0) ; 抛出 ArithmeticException
(catch ArithmeticException e
"除零错误")))
; result2 => "除零错误"
抛出异常
主动抛出异常使用 throw 函数,传入一个异常实例:
(throw (ex-info "自定义错误" {:code 400}))
ex-info 是 Clojure 提供的便捷函数,创建带消息和附加数据的 ExceptionInfo 对象。你也可以直接构造 Java 异常:
(throw (IllegalArgumentException. "参数无效"))
实战:安全读取文件
组合 try、catch、finally 实现健壮的文件读取函数:
(defn safe-slurp [path]
(let [stream (java.io.FileInputStream. path)
reader (java.io.InputStreamReader. stream "UTF-8")
buffer (java.io.BufferedReader. reader)]
(try
(loop [lines [] line (.readLine buffer)]
(if (nil? line)
(clojure.string/join "\n" lines)
(recur (conj lines line) (.readLine buffer))))
(catch java.io.IOException e
(println "读取文件失败:" path (.getMessage e))
nil)
(finally
(.close buffer)))))
此函数:
- 打开文件流并包装为缓冲读取器。
- 逐行读取内容并拼接。
- 捕获
IOException并返回nil。 - 确保
buffer总是被关闭。
常见陷阱与最佳实践
-
避免空 catch 块
永远不要写(catch Exception _)而不做任何处理。至少记录日志:(catch Exception e (println "忽略异常:" e)) ; 不推荐,但比完全静默好 -
优先使用 ex-info
创建异常时尽量用ex-info,它支持携带任意数据,便于调试:(catch clojure.lang.ExceptionInfo e (let [data (ex-data e)] (println "错误详情:" data))) -
不要在 finally 中改变控制流
避免在finally中使用throw或return(Clojure 中即函数返回),这会干扰异常传播或正常返回。 -
考虑 with-open 宏
对于资源管理,优先使用with-open,它自动处理try-finally:(with-open [rdr (clojure.java.io/reader "/path/to/file")] (reduce str (line-seq rdr)))等价于手动
try-finally关闭rdr,但更简洁安全。
异常类型参考表
以下是在 Clojure 中常见的 Java 异常类型及其典型触发场景:
| 异常类型 | 触发场景 | 示例 |
|---|---|---|
java.lang.ArithmeticException |
算术错误(如除零) | (/ 1 0) |
java.lang.ClassCastException |
类型转换失败 | (int "hello") |
java.lang.NullPointerException |
调用 nil 对象方法 | (.length nil) |
java.lang.IllegalArgumentException |
参数非法 | (subs "abc" 0 10) |
java.io.FileNotFoundException |
文件不存在 | (slurp "/missing") |
clojure.lang.ExceptionInfo |
由 ex-info 显式抛出 |
(throw (ex-info "msg" {:k :v})) |

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