文章目录

Clojure 异常处理:try、catch、finally

发布于 2026-04-03 20:57:32 · 浏览 174 次 · 评论 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 必须至少包含一个 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 表达式的返回值来源至关重要:

  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. "参数无效"))

实战:安全读取文件

组合 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 总是被关闭。

常见陷阱与最佳实践

  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)

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

扫一扫,手机查看

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