引言:为什么标准的 try/catch 不够用?
\n\n使用 Kotlin 进行开发,尤其是在服务器端应用或 Android 的背景下,经常需要可靠的错误处理。标准的 try/catch 块是基础,但在实践中,它会导致逻辑分散并增加测试的复杂性。在高级层面上,我们不仅仅是想捕获异常,而是希望设计系统,使错误成为数据类型的一部分,而不是意外的控制流。
在本文中,我们将探讨如何超越 try/catch,使用函数式方法:Result、Either(来自库)和 sealed class。您将学习如何使代码可预测、类型安全且易于维护。
1. 经典的 try/catch:它什么时候仍然有用?
\n\n不必完全放弃 try/catch。它对于处理遗留代码、外部库(例如,解析 JSON 时)以及系统调用(输入/输出)是不可或缺的。然而,有一些重要的细微差别。
正确的结构和资源管理
\n与 Java 不同,Kotlin 没有受检异常,但有一个优雅的 use 结构用于自动关闭资源:
fun readFile(path: String): String? = try {\n BufferedReader(FileReader(path)).use { it.readText() }\n} catch (e: IOException) {\n println("读取文件错误:${e.message}")\n null\n}\n\n这里的 use 保证了文件的关闭,即使内部发生了异常。这是对 try-with-resources 的优雅替代。
问题:上下文丢失
\ntry/catch 的主要缺点是它不会告知调用代码该函数可能“失败”。异常是一个隐藏的副作用。调用 readFile() 可能返回 null 或抛出 IOException,但函数签名并未反映这一点。这会导致运行时错误。
解决方案是转向显式的错误表示。
\n\n2. Result:用于成功或失败的内置单子
\n\nKotlin 提供了标准的 Result<T> 类。它是一个简单的代数结构:要么是成功(值 T),要么是失败(Throwable)。这使得错误处理成为类型系统的一部分。
基本用法
\nfun riskyDivision(a: Int, b: Int): Result<Int> = runCatching {\n a / b\n}\n\nfun main() {\n val result = riskyDivision(10, 0)\n result.onSuccess { println("结果:$it") }\n .onFailure { println("错误:${it.message}") }\n}\n\nrunCatching 是 try/catch 的一个包装器,它返回一个 Result。方法 onSuccess 和 onFailure 允许您对结果做出反应,而不会中断调用链。
转换和链式调用
\n您可以转换结果,而无需担心 map 内部的异常:
\n\nval finalResult = riskyDivision(10, 2)\n .map { it * 2 } // 如果成功——乘以 2\n .recover { error -> // 如果错误——返回备用值\n println("由于 ${error.message},使用备用值")\n 0\n }\nprintln(finalResult.getOrDefault(-1)) // 10\n\n重要提示: Result 不适合在没有额外处理的情况下存储在集合中或在线程之间传递。对于这些目的,sealed class 或像 Arrow 这样的库更合适。
3. Either:区分错误类型
\n\nResult 类只固定了 Throwable。但我们经常希望区分错误类型:“未找到”、“权限不足”、“验证错误”。这时,来自函数式库(例如 Arrow 或自定义