我们中的大多数人已经从Java转到了Kotlin。当我们开始使用Kotlin进行开发时,我们自然而然地采用了Java的做法。
我们开始尝试:首先是尝试避免可为空类型,然后是尝试使用数据类并学习扩展函数。在某个时候,我们变得渴望探索新的方法来实现其他所有事情。
本文是我学习Kotlin的旅程的一部分。我想发现除了try/catch块之外的其他错误处理技术,并深入探讨在我们的Kotlin应用程序中处理错误的不同技术。
让我们开始吧! :)
这篇内容的YouTube封面
Java方式
让我们一起实现一个简单的应用程序。
我们将首先实现一个函数,该函数将负责读取文件的内容:
fun readContentFromFile(filename: String): String {
return try {
File(filename).readText()
} catch (e: IOException) {
println("Error reading the file: ${e.message}")
throw e
}
}
我们的函数尝试从文件中读取文本,如果出现任何问题,我们会打印错误消息,然后引发异常。
然后,让我们实现另一个函数来转换该文件的内容并返回两个数字:
fun transformContent(content: String): CalculationInput {
val numbers = content.split(",").mapNotNull { it.toIntOrNull() }
if (numbers.size != 2)
throw Exception("Invalid input format")
return CalculationInput(numbers[0], numbers[1])
}
transformContent
函数首先使用逗号作为分隔符拆分文本,然后将我们的块转换为int。导致一个int列表。
然后,我们检查此列表中是否只有两个数字,如果不是,则引发异常,说明我们具有无效的输入格式。
否则,我们返回一个CalculationInput对象,该对象将保存我们的两个数字以进行进一步计算。
这是我们的CalculationInput类的样子:
data class CalculationInput(val a: Int, val b: Int)
有了这个对象,我们就可以调用我们的divide函数,该函数将第一个数字除以第二个数字并吐出此计算的商:
fun divide(a: Int, b: Int): Int {
if (b == 0)
throw ArithmeticException("Division by zero is not allowed")
return a / b
}
在这个函数中,我们首先检查除数是否为零,如果是,就引发异常。否则,我们只返回我们的除法商。
太棒了,让我们将所有内容组合在我们的main函数中,并用try/catch块将所有内容包围起来。
fun main() {
try {
val content = readContentFromFile("input.txt")
val numbers = transformContent(content)
val quotient = divide(numbers.a, numbers.b)
println("The quotient of the division is $quotient")
} catch (e: IOException) {
println("Error reading the file: ${e.message}")
} catch (e: Exception) {
println("Error: ${e.message}")
}
}
和你一样,我也用怀疑的眼光看待它。这是Kotlin方式吗?
好的,让我们看看替代方案。
发现Kotlin方式
runCatching
我们要看的第一种方法是runCatching
上下文。
让我们重构我们的主函数,看看它会是什么样子:
runCatching {
val content = readContentFromFile("input.txt")
val numbers = transformContent(content)
val quotient = Calculator().divide(numbers.a, numbers.b)
println("The quotient of the division is $quotient")
}.onFailure {
println("Error: ${it.message}")
}
runCatching
允许我们通过封装单个函数调用中的异常处理来编写更紧凑和可读的代码。提高了代码的简洁性。
此外,它促进了一种函数式编程风格,使得更容易链接操作并以更符合惯例的_Kotlin_方式处理结果。
此外,runCatching
上下文返回一个显式的结果类型,表示操作的成功或失败,清楚地说明了在调用代码中应该如何处理错误。
为了展示这个显式的结果类型,我们可以重构我们的代码,使其看起来像下面这样:
fun main() {
val result = runCatching {
val content = readContentFromFile("input.txt")
val numbers = transformContent(content)
val quotient = Calculator().divide(numbers.a, numbers.b)
println("The quotient of the division is $quotient")
}
result.onFailure {
println("Error: ${it.message}")
}
}
**Result
**类型更加强大。这就是我们接下来要看到的。
Result
为了展示Result
类型,我们要更深入地重构我们的应用程序。
让我们首先更改函数的返回类型以返回Result类型。
例如,我们的readContentFromFile函数现在将返回一个类型为String:
的Result:
fun readContentFromFile(filename: String): Result<String> {
return try {
Result.success(
File(filename).readText()
)
} catch (e: IOException) {
Result.failure(e)
}
}
现在,我们的函数将返回我们的内容包装在一个Result对象内或将我们的异常包装在一个Result.Failure对象内。
让我们对我们的其他函数执行相同的操作:
fun transformContent(content: String): Result<CalculationInput> {
val numbers = content.split(",").mapNotNull { it.toIntOrNull() }
if (numbers.size != 2) {
return Result.failure(Exception("Invalid input format"))
}
return Result.success(CalculationInput(numbers[0], numbers[1]))
}
请确保你不再在函数中抛出异常,而是将异常返回到Result.failure()
中。
fun divide(a: Int, b: Int): Result<Int> {
if (b == 0)
return Result.failure(Exception("Division by zero"))
return Result.success(a / b)
}
到目前为止,一切都很好。
现在,这就是有趣的地方。Result
是一个灵活的类型,可以以不同的方式处理自己。 让我们重构我们的main
函数并探索这些不同的方式。
Fold: 第一个是fold
,这是一个需要我们明确处理成功和失败情况的函数。
fun main() {
val content = readContentFromFile("input.txt")
content.fold(
onSuccess = {
// Do something with the content of the file
},
onFailure = {
println("Error reading the file: ${it.message}")
}
)
}
在我们的情况下,每次结果都需要再次调用fold,最终得到一个嵌套结构:
fun main() {
readContentFromFile("input.txt").fold(
onSuccess = {content ->
transformContent(content).fold(
onSuccess = {numbers ->
divide(numbers.a, numbers.b).fold(
onSuccess = {quotient ->
println("The quotient of the division is $quotient")
},
onFailure = {
println("Error: ${it.message}")
}
)
},
onFailure = {
println("Error: ${it.message}")
}
)
},
onFailure = {
println("Error reading the file: ${it.message}")
}
)
}
是的…那看起来不太好。让我们尝试映射它们:
Map:
fun main() {
readContentFromFile("input.txt").map { content ->
transformContent(content).map { numbers ->
divide(numbers.a, numbers.b)
}
} }.fold(
onSuccess = { content ->
content.fold(
onSuccess = { numbers ->
numbers.fold(
onSuccess = { quotient ->
println("The quotient of the division is $quotient")
},
onFailure = {
println("Error: ${it.message}")
}
)
},
onFailure = {
println("Error: ${it.message}")
}
)
},
onFailure = {
println("Error: ${it.message}")
}
)
}
看起来好多了,是吗?不幸的是,这里没有flatMap
函数,所以我们最终得到了Result<Result<Int>>
**。**需要我们再次多次嵌套我们的代码:
fun main() {
readContentFromFile("input.txt").map { content ->
transformContent(content).map { numbers ->
divide(numbers.a, numbers.b)
}
} }.fold(
onSuccess = { content ->
content.fold(
onSuccess = { numbers ->
numbers.fold(
onSuccess = { quotient ->
println("The quotient of the division is $quotient")
},
onFailure = {
println("Error: ${it.message}")
}
)
},
onFailure = {
println("Error: ${it.message}")
}
)
},
onFailure = {
println("Error: ${it.message}")
}
)
}
使用结果类可以使错误处理更加明确,更易于阅读和维护,并且与传统的try-catch块相比,更不容易出现隐藏的错误。**然而,它可能被视为有些有争议,因为它要求我们处理成功和失败路径,有人可以说它是将已检查的异常重新引入Kotlin中。
那么对我们来说还剩下什么?
Arrow方式
Arrow是一个用于Kotlin的函数式编程库,提供了一组强大的抽象,用于处理函数式数据类型。
**其中一些构造是扩展Result类功能的函数,允许开发人员决定是否要显式处理失败路径。**这使得在某些情况下,错误处理更加简单明了,不那么复杂。今天我们要探讨的两个构造是 flatMap
函数和 result
上下文。
让我们将 Arrow 添加到我们的依赖项中:
dependencies {
implementation("io.arrow-kt:arrow-core:1.2.0-RC")
}
然后让我们再次重构我们的 main
函数:
flatMap:
正如我们之前讨论的那样,Result
类型本身提供了 map
函数。然而,当映射多个 Result
对象时,我们最终会得到结果的结果(Result< Result< Int>>
)。Arrow 通过提供 flatMap
函数增强了 Result
类型的功能,允许我们最终只得到一个结果:
fun main() {
val result = readContentFromFile("input.txt").flatMap { content ->
transformContent(content).flatMap { numbers ->
divide(numbers.a, numbers.b)
}
}
result.fold(
onSuccess = {quotient ->
println("The quotient of the division is $quotient")
},
onFailure = {
println("Error: ${it.message}")
}
)
}
result 上下文:
result
函数是一个包装器,它在 Result
上下文中执行其代码块,捕获任何异常并将它们包装在 Failure
对象中。
bind()
方法用于解包 Result
。如果 Result
是 Success
,则解包该值;如果是 Failure
,则停止执行并传播错误。
fun main() {
result {
val content = readContentFromFile("input.txt").bind()
val numbers = transformContent(content).bind()
val quotient = divide(numbers.a, numbers.b).bind()
println("The quotient of the division is $quotient")
}.onFailure {
println("Error: ${it.message}")
}
}
这些方法使我们的代码比传统的 try/catch 块更加清晰和易于理解。但是...
有个问题
我们应该在业务代码中捕获异常吗?正如我们之前所看到的,Kotlin 提供了 Result 类和 runCatching 函数来进行更具惯用性的错误处理。然而,考虑何时何地使用这些机制非常重要。
例如,runCatching 捕获各种 Throwable,包括 JVM 错误,如 NoClassDefFoundError、ThreadDeath、OutOfMemoryError 或 StackOverflowError。通常,应用程序不应尝试从这些严重问题中恢复,因为往往很难解决。像 runCatching 这样的“捕获所有”机制不建议用于业务代码,因为它们可能会使错误处理变得不清晰和复杂。
此外,重要的是要区分业务代码中预期的错误和意外的逻辑错误。虽然可以处理和恢复预期的错误,但意外的逻辑错误通常表明需要修复代码逻辑的编程错误。将两种类型的错误处理方式相同,可能会导致混淆并使代码难以维护。
getOrThrow()
Result 类还提供了一个 getOrThrow()
函数。该函数将返回预期的值或抛出异常。让我们看看它是如何工作的:
fun main() {
val content = readContentFromFile("input.txt").getOrThrow()
val numbers = transformContent(content).getOrThrow()
val quotient = divide(numbers.a, numbers.b).getOrThrow()
println("The quotient of the division is $quotient")
}
对于我们的大多数业务代码,这是我们应该遵循的方法。异常意味着我们的代码有问题。如果我们的代码有问题,我们应该修复我们的代码。
但是,你可能会问:为什么还要使用 Result?
Kotlin 方式
最终,通过不返回 Result 类型,只允许异常在我们的代码中上升,我们将实现相同的结果:
fun main() {
val content = readContentFromFile("input.txt")
val numbers = transformContent(content)
val quotient = Calculator().divide(input.a, input.b)
println("The quotient of the division is $quotient")
}
如果文件不存在或输入不正确,我们最终仍将抛出异常。
事实上,对于我们的大多数业务代码,我们不必担心捕获异常。
“作为经验法则,你不应该在一般 Kotlin 代码中捕获异常。这是代码味道。异常应该由应用程序的一些顶层框架代码处理,以警告开发人员代码中的错误并重新启动应用程序或其受影响的操作。这是 Kotlin 中异常的主要目的。“—Roman Elizarov(Kotlin 编程语言项目负责人)
正如我们在前一节中讨论的那样,通常在业务代码中捕获异常是没有意义的,因为它们出现在开发人员犯错误并且代码的逻辑是错误的时候。
我们应该修复代码逻辑而不是捕获不可恢复的异常。
结论
在 Kotlin 中,传统的 try-catch 块可能会使你的代码更难以阅读和维护。相反,语言鼓励使用更具惯用性的错误处理技术,例如 Result 类和 runCatching 函数,以提高代码的可读性和可维护性。
但是,重要的是要区分代码中预期的错误和意外的逻辑错误,并决定何时何地使用错误处理机制。类库,如 Arrow,可以提供额外的工具,使错误处理更加简单和不混乱。
通过遵循这些最佳实践并使用适当的工具,你可以编写更可读、可维护和有效的 Kotlin 代码。
大多数情况下,最好的选择是保持代码简单,不要过于复杂 😁
GitHub 上的示例:
GitHub - raphaeldelio/Kotlin-Error-Handling-Examples
资源
- Kotlin 和异常 by Roman Elizarov (https://elizarov.medium.com/kotlin-and-exceptions-8062f589d07)
- Kotlin 中何时以及如何使用 Result? (https://stackoverflow.com/questions/70847513/when-and-how-to-use-result-in-kotlin)
- Kotlin 和函数式编程:选择最好的,跳过其余的 by Urs Peter (https://youtu.be/Zz8zl4v2XXs)
译自:https://raphaeldelio.medium.com/exceptions-in-kotlin-beyond-the-try-catch-c73d8556e0e0
评论(0)