文章目录

Clojure 异常处理:try、catch、finally

发布于 2026-04-03 20:57:32 · 浏览 2 次 · 评论 0 条

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 必须至少包含一个 catchfinally 子句。
  • 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 表达式的返回值来源至关重要

  1. 如果 try 主体正常完成,其最后一个表达式的值就是整个 try 的返回值。
  2. 如果发生异常且被某个 catch 捕获,该 catch 块的最后一个表达式的值成为返回值。
  3. 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. "参数无效"))

实战:安全读取文件

组合 trycatchfinally 实现健壮的文件读取函数

(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 总是被关闭

常见陷阱与最佳实践

  1. 避免空 catch 块
    永远不要写 (catch Exception _) 而不做任何处理。至少记录日志:

    (catch Exception e
      (println "忽略异常:" e)) ; 不推荐,但比完全静默好
  2. 优先使用 ex-info
    创建异常时尽量用 ex-info,它支持携带任意数据,便于调试:

    (catch clojure.lang.ExceptionInfo e
      (let [data (ex-data e)]
        (println "错误详情:" data)))
  3. 不要在 finally 中改变控制流
    避免在 finally 中使用 throw return(Clojure 中即函数返回),这会干扰异常传播或正常返回。

  4. 考虑 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}))

评论 (0)

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

扫一扫,手机查看

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