我们大多数人已经从Java过渡到Kotlin。当我们开始使用Kotlin开发时,我们的自然方法是按照我们在Java的方式做事。
我们开始进行实验:首先,试图避免使用无效的类型,然后使用数据类并了解扩展功能。在某个时候,我们渴望探索实施其他一切的新方法。
本文是我学习Kotlin的旅程的一部分。我想发现超出我们的尝试/捕获块的方法,并深入研究不同的技术来处理我们的Kotlin应用程序中的错误。
让我们开始! :)
爪哇方式
让我们一起实现一个简单的应用程序。
我们将开始实施一个函数,该函数将负责读取文件的内容:
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
函数首先使用逗号作为定界符将文本拆分,然后将我们的块转换为ints。导致ints的列表
然后,我们检查此列表中是否只有两个数字,如果不是这种情况,我们提出了一个例外,指出我们有无效的输入格式。
否则,我们返回一个计算input对象,该对象将持有我们的两个数字以进行进一步的计算。
这是我们的计算input类的样子:
data class CalculationInput(val a: Int, val b: Int)
使用该对象,我们可以调用我们的分隔函数,该功能将第一个数字除以第二个数字并吐出此计算的商:
fun divide(a: Int, b: Int): Int {
if (b == 0)
throw ArithmeticException("Division by zero is not allowed")
return a / b
}
在此函数中,我们首先检查除数是否等于零,如果这是正确的,则提出了例外。否则,我们只返回我们部门的商。
很酷,让我们将所有内容结合在我们的主要功能中,并用尝试/捕获块包围所有内容。
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 Way。
此外,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
类型,我们将更深入地重构我们的应用程序。
让s 首先更改函数的返回类型以返回结果类型。
例如,我们的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}")
}
)
}
在我们的情况下,每个结果,我们都必须再次致电折叠,最终以嵌套结构:
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}")
}
)
}
是的,看起来不太好。 让我们尝试映射它们:
地图:
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。
那么我们剩下什么?
箭头方式
箭头是Kotlin的功能编程库,为使用功能数据类型的功能提供了一组强大的抽象。
其中一些构造是扩展结果类功能的功能,并允许开发人员决定是否要明确处理失败路径。这使得错误处理更加直接,并且在某些方面更令人费解情况。
我们今天要探索的两个结构是Flatmap函数和result
上下文。
让我们的依赖项添加箭头:
dependencies {
implementation("io.arrow-kt:arrow-core:1.2.0-RC")
}
和让我的重构我们的main
函数再次:
flatmap:
正如我们之前讨论的, Result
类型本质地提供了map
函数。但是,当映射多个结果对象时,我们最终得到结果的结果(Result<Result<Int>>
)。
箭头通过提供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
上下文中执行其代码块,捕获任何例外并将其包裹在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}")
}
}
与传统的尝试/捕获块相比,这些方法使我们的代码更加清洁,更容易推理。但是
有一个捕获
我们应该完全在业务代码中捕获例外吗?,正如我们之前看到的,Kotlin提供了结果类和RunCatching功能,以实现更惯用的错误处理。但是,必须考虑何时何地使用这些机制。
例如,RunCatching会捕获各种各样的可投掷,包括NoclassDeffoundError,ThreadDeath,OutofMemoryError或StackoverFlowerRor等JVM错误。 通常,应用程序不应尝试从这些严重的问题中恢复,因为通常几乎无法做到解决这些问题。 catch-alling All机制不建议使用Runcatching商业代码,因为它们可以使错误处理不清楚且令人费解。
此外,必须区分预期错误和商业代码中意外的逻辑错误。虽然可以从中处理和恢复预期错误,但意外的逻辑错误通常表明需要修复代码的编程错误S逻辑。 以相同的方式处理两种错误,可能导致混乱并使代码难以维护。
getorthrow()
结果类还提供了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")
}
对于我们的大多数业务代码,这是我们应该遵循的方法。 一个例外意味着我们的代码存在问题。如果我们的代码有问题,我们应该修复我们的代码。
但是,您可能会问:为什么根本会产生?
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编程语言的项目负责人)
正如我们在上一节中讨论的那样,捕获业务代码中的例外通常是没有意义的,因为当开发人员犯了错误并且逻辑逻辑时,它们会出现。
。而不是捕获无法恢复的异常,我们应该修复代码的逻辑。
结论
在Kotlin中,传统的TryCatch块可以使您的代码更难读取和维护。相反,该语言鼓励使用更惯用的错误处理技术(例如结果类和捕获功能)来提高代码可读性和可维护性。
但是,要区分代码中的预期错误和意外的逻辑错误,并确定使用错误处理机制的何时何地,至关重要。像箭头这样的库可以提供其他工具,以使错误处理更加简单且令人费解。
通过遵循这些最佳实践并使用适当的工具,您可以编写更可读,可维护和有效的Kotlin代码。
大多数时候,最好的选择是保持代码简单而不是过度复杂化ð
Github上的示例:
Kotlin Error Handling Examples
In this repository I explore different approaches for error handling in Kotlin. Namely:
- try/catch blocks
- Result class
- runCatching context
- Result Context (Arrow library)
- Either class (Arrow library)
- Nothing at all
贡献
写作需要时间和精力。我喜欢写作和分享知识,但我也有账单要付。如果您喜欢我的工作,请,考虑通过购买我的咖啡捐赠:https://www.buymeacoffee.com/RaphaelDeLio
或通过发送我的比特币:1HJG7PMGHG3Z8RATH4AIUWR156BGAFJ6ZW
我很高兴您能进入这个故事的这一部分,我真的很感谢。
支持我的工作:跟随我并鼓掌这个故事。
来源
- Kotlin和Roman Elizarov的例外 (https://elizarov.medium.com/kotlin-and-exceptions-8062f589d07)
- 何时以及如何使用kotlin? (https://stackoverflow.com/questions/70847513/when-and-how-to-use-result-in-kotlin)
- Kotlin&功能编程:选择最佳,跳过Urs Peter(https://youtu.be/Zz8zl4v2XXs)的其余部分
在YouTube上观看