
在编写 Kotlin 代码时,你最终会遇到处理异常的不同方法,或者更笼统地说,处理可能会失败的函数的不同方法。
一种常见的方法是使用 try-catch
块,就像在许多其他编程语言(例如 Java)那样。
然而,Kotlin 引入了一些其他便捷方法,这些方法能让异常处理更加灵活和简洁。其中一种就是 runCatching
。
在本文中,我们将详细探讨 Kotlin 的 runCatching
。我们会分析它与传统 try-catch
块有何不同,以及它是否真的带来了实际好处,还是说仅仅是新瓶装旧酒。通过这次探讨,应能帮助你判断 runCatching
是否适合你的编码风格或项目需求。
try-catch:传统的错误处理

在讨论 runCatching
之前,我们看下传统的 Kotlin 错误处理方式。
传统上,错误处理通常围绕代码中出现的异常展开,在 Kotlin 中处理异常的方式与在 Java 中相同:
kotlin
try {
// 可能会抛出异常的代码
} catch (e: Exception) {
// 处理异常
} finally {
// 无论是否抛出异常都应该运行的代码
}
try-catch
是怎么用的呢?
try
块:将你认为可能会抛出异常的代码放在try
块中。catch
块:如果抛出了某种类型的异常,程序流程会跳转到这里来处理错误。你可以选择记录异常、重新抛出异常,或者以其他方式处理它。finally
块:这个块是可选的。无论try
块或catch
块中发生了什么(除非完全终止应用程序),finally
块中的代码都会运行。通常,这用于清理资源,比如关闭数据库连接。
如果你熟悉 IO 操作的话,那么你一定知道 try-catch
的用法,尤其不要忘了在 finally
块中关闭流。
runCatching:新颖的错误处理

Kotlin 提供了一些高阶函数来处理可能会失败的操作,其中就有 runCatching
。从这个名字我们就能略知一二:"run" 指的是执行某些代码,"catching" 指的是捕获这些代码可能抛出的异常。
所以从根本上说,runCatching
是一个执行给定代码块并封装结果的函数。如果你的代码块运行失败,runCatching
会捕获异常;如果运行成功,它则会捕获成功的结果。
你可以将 runCatching
视为 Kotlin 采用的一种更具函数式风格的错误处理方式。它返回一个 Result<T>
,在很多 Kotlin 版本中,Result<T>
本质上是一个密封类,它既可以保存成功的结果(Success
),也可以保存失败的结果(Failure
)。
下面是一个 runCatching
的简单示例:
kotlin
val result: Result<Int> = runCatching {
// 一些可能会抛出异常的代码
10 / 2
}
在这段代码中,如果表达式 10 / 2
执行成功,result
将保存 Success(5)
。如果执行过程中抛出异常,result
将保存 Failure(e)
,其中 e
代表具体的异常。
这个方法妙就妙在返回的 result
始终是 Result<Int>
类型,因此在紧接着的代码中你不需要使用不同的代码块来处理不同情况。
接下来,你可以以一种更加声明式的风格来使用 result
处理成功或失败的情况。我们给出复杂一点的例子,让我们尝试一些更贴近文件读取场景的操作:
kotlin
import java.io.File
import java.io.IOException
fun readFileUsingRunCatching(filePath: String): Result<String> {
return runCatching {
val file = File(filePath)
file.readText()
}
}
fun main() {
val result = readFileUsingRunCatching("/path/to/file.txt")
result
.onSuccess { content -> println("文件内容: $content") }
.onFailure { exception -> println("读取文件失败: ${exception.message}") }
}
上述代码中,readFileUsingRunCatching
函数返回一个 Result<String>
。
- 如果文件读取成功,结果将是包含文件内容的
Success
。 - 如果读取失败,结果将是包含异常的
Failure
。
然后,在 main
函数中,我们可以通过链式调用来处理成功或失败的情况:
onSuccess {...}
在操作成功时调用。onFailure {...}
在发生异常时调用 。
注意,在读取文件的函数中,我们避免编写显式的 try-catch
块。我们仅依靠 runCatching
来捕获任何抛出的异常。
这种风格可以让你的代码看起来更加简洁流畅,尤其当你喜欢链式函数调用或者采用更具函数式风格的编程时。
新老的碰撞

现在我们对 runCatching
的工作原理有了大致的了解,让我们来探讨一下:runCatching
仅仅是新瓶装旧酒,还是说它真的能带来额外的好处?
相似之处:
- 两者都能捕获异常:无论你使用
try-catch
还是runCatching
,它们都能在出现问题时,捕获异常。 - 两者都不能消除异常的可能性:即使
runCatching
返回一个Result
对象,你仍然需要处理代码某些部分可能失败的情况。 - 两者都能处理清理工作:你可以通过对
Result
对象进行链式调用来重现一些原本放在finally
块中的逻辑(比如使用onFailure
来释放资源)。
差异
-
返回类型:
try-catch
本身不会生成一个可以存储和传递的显式类型。当然开发者可以手动返回不同的值(比如失败时返回null
,成功时返回实际值),但这需要你自己去实现;runCatching
始终返回一个Result<T>
,这意味着你得到了一个标准的包装对象,你可以传递它、对其进行链式调用,或者使用map
、recover
等扩展函数对其进行转换。 -
代码可读性和函数式风格:使用
try-catch
时,如果存在多个可能的失败点,代码可能会变得更加嵌套或杂乱。最终你可能会编写多个try-catch
块,或者将逻辑堆积在一个块中,编写多个catch
块;使用runCatching
时,代码流程通常更具线性。因为在包装操作之后,你可以在Result
上链式调用方法。它更具声明性,能清楚地展示如何处理成功和失败情况,以及如何转换结果或从错误中恢复。 -
链式调用和转换:
try-catch
通常意味着 "停止正常流程,进入catch
块,然后继续执行";runCatching
能很好地与转换操作集成。你可以这样做:kotlinrunCatching { /* 操作 */ } .map { /* 成功时进行转换 */ } .recover { /* 失败时进行回退 */ } .onSuccess { /* 执行某些操作 */ } .onFailure { /* 执行某些操作 */ }
通过这种方法,你可以将操作以管道形式链式连接,而不是使用块来控制流程。
-
冗长程度,如果只有一两行可能抛出异常的代码,
try-catch
相当容易理解。然而,如果你要进行大量的转换操作,或者传递结果,最终可能会得到多个嵌套的try-catch
块,或者一个带有复杂逻辑的单一代码块;而runCatching
有助于减少样板代码,你不再需要将每一步都包装在try
块中。相反,你只需将整个操作包装一次,并将所有情况作为一个Result
来处理。
深入了解 Result<T>

runCatching
会返回一个 Result<T>
。
在 Kotlin 中,这个 Result
类既可以存储一个值(表示成功),也可以存储一个异常(表示失败)。它旨在帮助你以一种统一的方式处理这些结果,而不是让逻辑分散。
Result<T>
的常用方法:
isSuccess
/isFailure
:用于检查结果是成功还是失败的布尔值。getOrNull()
:如果操作成功,返回对应的值;如果失败,则返回null
。exceptionOrNull()
:如果操作失败,返回抛出的异常;如果成功,则返回null
。onSuccess(action: (T) -> Unit): Result<T>
:如果结果是成功,运行给定的lambda
表达式。onFailure(action: (Throwable) -> Unit): Result<T>
:如果结果是失败,运行给定的lambda
表达式。map(transform: (value: T) -> R): Result<R>
:如果操作成功,对值进行转换;如果失败,则传播失败结果。recover(transform: (Throwable) -> T): Result<T>
:如果操作失败,使用给定的lambda
表达式提供一个回退值,从而有效地从错误中恢复。
你可以链式调用这些方法,创建一个操作流程,无论是处理数据(操作成功时),还是处理错误(操作失败时)。这与传统的 try-catch
有很大不同,传统的 try-catch
通常在捕获到异常或完成 try
块后就结束了。
我们给出几个链式调用的示例。
假设你有一个从服务器获取用户对象的函数。如果获取到的用户对象有效,我们希望对其进行处理;否则,记录错误并返回一个默认用户。即使在恢复过程中出现问题,getOrNull
也会返回 null
。
kotlin
val userResult = runCatching { fetchUserFromServer() }
.map { user ->
// 转换用户对象,例如修剪字段或进行验证
user.copy(name = user.name.trim())
}
.recover { exception ->
// 如果出现问题,使用一个默认用户进行恢复
println("Error fetching user: ${exception.message}")
User(id = -1, name = "Guest")
}
val user = userResult.getOrNull()
println("User: $user")
这种方式可以使你的逻辑保持在一个连贯的流程中,而无需使用多个嵌套块。
在 runCatching
之后,我们在成功时进行转换,失败时用默认值。
下面来看一下如何使用 onSuccess
和 onFailure
:
kotlin
val userResult = runCatching {
fetchUserFromServer()
}.map { user ->
user.copy(name = user.name.trim())
}
userResult
.onSuccess { user ->
// 如果一切成功,打印修剪后的用户
println("User: $user")
}
.onFailure { exception ->
// 如果出现问题,记录错误并打印默认用户
println("Error fetching user: ${exception.message}")
val fallback = User(id = -1, name = "Guest")
println("User: $fallback")
}
使用 try/catch/finally
块实现同样功能的代码如下:
kotlin
fun main() {
var user: User? = null
try {
val fetchedUser = fetchUserFromServer()
val trimmedUser = fetchedUser.copy(name = fetchedUser.name.trim())
user = trimmedUser // 如果一切顺利,我们存储修剪后的用户
} catch (exception: Exception) {
println("Error fetching user: ${exception.message}")
user = User(id = -1, name = "Guest") // 失败时返回默认用户
} finally {
println("User: $user")
}
}
在使用 try-catch-finally
时,经常涉及资源管理的问题。例如,你在 try
块中打开了一个文件或建立了一个网络连接,无论发生什么,你可能都希望在 finally
块中关闭它。
这在 runCatching
中是如何实现的呢?
在很多情况下,Kotlin 有自己处理资源的模式,例如用于处理可关闭资源的 use
函数,它可以自动进行清理。但如果你确实需要一些自定义的 "清理" 逻辑,也有以下几种选择:
-
仅在失败时清理(在
onFailure
中),如果你只需要在出现问题时清理资源,可以在onFailure
中进行操作。假设我们有一个文件流,只有在读取失败时才需要丢弃或进行特殊处理:kotlinimport java.io.FileInputStream import java.io.IOException fun readWithCleanupOnFailure(filePath: String) { val stream = FileInputStream(filePath) runCatching { // 有风险的操作 val data = stream.readBytes() println("Read ${data.size} bytes") }.onFailure { exception -> println("Something went wrong: ${exception.message}") // 如果只在失败时关闭流 try { stream.close() } catch (closeException: IOException) { println("Error closing stream on failure: ${closeException.message}") } } // 如果没有异常,函数执行完后流仍保持打开状态 // (只有在你确实希望如此时才使用这种方法) }
我们首先打开
FileInputStream
。在.onFailure
中处理读取过程中可能出现的任何异常。 在这个块中关闭流。 如果一切成功,runCatching
执行完后流仍保持打开状态(通常你会在其他地方或以其他方式处理它)。如果你的资源只需要在出错时进行清理时,这种方法很有用。不过,通常情况下,你无论如何都需要关闭资源。
-
无论如何都进行清理
-
使用 Kotlin 的
use
扩展:Kotlin 的use
函数是关闭实现了Closeable
接口的资源的最常用模式。当lambda
表达式结束时,它会自动关闭资源,无论操作成功与否:kotlinimport java.io.FileInputStream import java.io.IOException fun readWithUse(filePath: String) { runCatching { FileInputStream(filePath).use { stream -> val data = stream.readBytes() println("Read ${data.size} bytes") // 流会自动关闭 } }.onFailure { exception -> println("Something went wrong: ${exception.message}") } }
use
确保在块结束后(包括抛出异常的情况)总会调用stream.close()
。 这就无需手动编写finally
块或手动调用关闭方法。 -
在整个结果链之后进行清理:你必须在
runCatching
之前打开资源,然后在之后无条件关闭它:kotlinimport java.io.FileInputStream import java.io.IOException fun readWithSeparateClose(filePath: String) { val stream = FileInputStream(filePath) val result = runCatching { val data = stream.readBytes() println("Read ${data.size} bytes") } // 执行无条件清理,此处依然可以使用 runCatching ! try { stream.close() } catch (e: IOException) { println("Failed to close stream: ${e.message}") } // 现在处理成功或失败的情况 result.onFailure { println("Operation failed: ${it.message}") }.onSuccess { println("Operation succeeded.") } }
在
runCatching
之前打开流。 在runCatching
中执行有风险的操作。 然后在普通的try/catch
中无条件关闭流。 最后,检查Result
来处理成功或失败的情况。这类似于
finally
块,但使用了两个不同的步骤:runCatching
用于捕获错误,随后的块用于确保资源被清理。 -
使用
also {... }
进行清理:kotlinimport java.io.FileInputStream import java.io.IOException fun readWithAlso(filePath: String) { val stream = FileInputStream(filePath) val result = runCatching { // 有风险的操作 val data = stream.readBytes() println("Read ${data.size} bytes") // 返回某些值或进行更多处理... }.also { // 无论上述操作成功还是失败,这个块都会运行。 // 非常适合无条件清理或日志记录: try { stream.close() } catch (e: IOException) { println("Error closing stream: ${e.message}") } println("Cleanup or logging done here.") } result .onFailure { println("Operation failed: ${it.message}") } .onSuccess { println("Operation succeeded.") } }
val result = runCatching {... }
:我们执行一个可能成功(读取字节)或失败(抛出异常)的块。操作结果会包装在一个Result
中。.also {... }
:also
函数接受一个 lambda 表达式,该表达式作用于Result
,但重要的是,无论操作成功与否,它总是会运行。它会以Result
作为接收者被调用,所以我们可以在这里进行资源关闭或最终的日志记录。 这类似于传统try-catch-finally
链中的finally
块。 与只在一种情况下运行的onFailure
或onSuccess
不同,also
是无条件运行的。result.onFailure {... }.onSuccess {... }
:在also
中执行完无条件逻辑后,如果我们还想进一步记录日志或转换结果,可以继续处理成功或失败的情况。
-
那么,何时使用 also
进行清理呢?
-
无条件的最终操作:如果你有一个资源无论发生错误与否都必须清理,而且你不想在
runCatching
中嵌入try-finally
,那么使用also {... }
是一种简洁的方式,可以在结果生成后立即将清理逻辑集中处理。 -
日志记录:你可以使用
also {... }
来记录 "操作完成" 或者进行一些指标跟踪,因为它在成功和失败路径中都会被保证执行。
runCatching的最佳实践
保持代码可读性
runCatching
可以形成非常优雅的链式调用,但也可能导致极其冗长的调用链,从而难以理解。如果你的调用链超过了几步,考虑将其拆分为命名恰当的函数或扩展方法。这样,阅读你代码的人看到的将是一个逻辑清晰的流程,而不是一长串无休止的 lambda
表达式。
避免滥用链式调用
有时,直接使用 try-catch
会更易读。如果你只有一步操作可能失败,并且只想记录错误,那么简单的 try-catch
可能是最佳选择。当你想要存储结果或传递结果以进行进一步转换时,runCatching
的优势才会显现出来。
牢记资源管理
如果你在处理必须关闭的资源,如文件流或数据库连接,依靠 Kotlin 内置的结构,如 use
函数。如果你需要无条件清理,可以考虑编写一个小函数,在 runCatching
外部(或之后)进行清理,或者将清理逻辑放在 .also
块中。不要试图将 finally
的概念直接硬塞进 runCatching
中(本身并没有 finally
的概念),要接受 Kotlin 处理资源的方式。
总结
Kotlin 不断朝着更安全、更具表现力的习惯用法发展,以降低隐藏错误的风险。runCatching
就是这种演变的一个典型例子:它将传统的概念(可能抛出异常的操作)包装在一个现代的结构(Result<T>
)中,使你能够在统一的流程中进行链式调用、转换和处理错误。
如果你曾发现自己的代码中充满了重复的 try-catch
块,或者通过返回 null
或特殊标志来表示错误,那么 runCatching
提供了一种更优雅的解决方案。
如果你是 Kotlin 的新手,可以通过一些 runCatching
的小例子来练习,看看它与 try-catch
有何不同。
如果你是 Kotlin 开发团队的负责人,制定一个编码标准,让每个人都知道何时使用 runCatching
,何时使用传统的 try-catch
。代码库的一致性对于可读性至关重要。
希望这篇深入的讨论不仅澄清了 runCatching
和 try-catch
之间的区别,还阐明了 Result<T>
是如何构建一种函数式的错误处理风格,以及你在哪里仍然可以添加最终操作来模仿传统的 finally
块。
现在的你,应该已经完全了解 runCatching
了吧!