我们中的大多数人已经从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函数现在将返回一个类型为Result
的String:
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}")
}
)
}
使用result类可以使错误处理更明确、更易于阅读和维护,并且比传统的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 这样的 catch-all 机制不推荐用于业务代码,因为它们会使错误处理变得不清晰和复杂。
此外,重要的是要区分业务代码中的预期错误和意外逻辑错误。虽然预期错误可以处理和恢复,但意外的逻辑错误通常表示需要修复代码逻辑的编程错误。在相同的方式处理这两种类型的错误可能会导致混淆,并使代码难以维护。
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
译自:https://raphaeldelio.medium.com/exceptions-in-kotlin-beyond-the-try-catch-c73d8556e0e0
评论(0)