异常
词语“异常”在这里的意义与短语“I take exception to that.”相同。
异常条件会阻止当前函数或作用域的继续执行。在问题发生的地方,您可能不知道该如何处理,但在当前上下文中无法继续执行。您没有足够的信息来修复问题。因此,您必须停止并将问题交给另一个能够采取适当行动的上下文。
本节介绍了异常作为错误报告机制的基础知识。在第 VI 部分:防止失败中,我们将介绍其他处理问题的方法。
很重要的一点是要区分异常条件和正常问题。正常问题在当前上下文中有足够的信息来应对问题。对于异常条件,您无法继续处理。您只能离开,将问题委托给外部上下文。这就是当您抛出异常时发生的情况。异常是从错误发生点“抛出”的对象。
考虑一下 toInt()
,它将一个 String
转换为 Int
。如果您对一个不包含整数值的 String
调用此函数会发生什么?
// Exceptions/ToIntException.kt
package exceptions
fun erroneousCode() {
// Uncomment this line to get an exception:
// val i = "1$".toInt() // [1]
}
fun main() {
erroneousCode()
}
取消对 [1] 行的注释会产生一个异常。在这里,失败的行被注释掉,以免停止本书的构建,该构建检查每个示例是否按预期编译和运行。
当抛出异常时,执行路径(无法继续执行的路径)会停止,并且异常对象会从当前上下文中弹出。在这里,它退出了 erroneousCode()
的上下文,然后进入了 main()
的上下文。在这种情况下,Kotlin 仅报告错误;程序员可能犯了一个错误,必须修复代码。
当异常未被捕获时,程序会中止,并显示包含详细信息的堆栈跟踪。在 ToIntException.kt
中取消注释 [1] 行,会产生以下输出:
Exception in thread "main" java.lang.NumberFormatException: For input s\
tring: "1$"
at java.lang.NumberFormatException.forInputString(NumberFormatExcepti\
on.java:65)
at java.lang.Integer.parseInt(Integer.java:580)
at java.lang.Integer.parseInt(Integer.java:615)
at ToIntExceptionKt.erroneousCode(at ToIntException.kt:6)
at ToIntExceptionKt.main(at ToIntException.kt:10)
堆栈跟踪提供了详细信息,比如异常发生的文件和行,这样您就可以快速发现问题所在。最后两行显示了问题:在 main()
的第 10 行我们调用了 erroneousCode()
。然后,更准确地说,在 erroneousCode()
的第 6 行我们调用了 toInt()
。
为了避免注释和取消注释代码以显示异常,我们使用了 AtomicTest
包中的 capture()
函数:
// Exceptions/IntroducingCapture.kt
import atomictest.*
fun main() {
capture {
"1$".toInt()
} eq "NumberFormatException: " +
"""For input string: "1$""""
}
使用 capture()
,我们将生成的异常与预期的错误消息进行比较。capture()
对于正常编程不是很有用——它专门为本书设计,以便您可以看到异常,并知道输出已经经过了本书的构建系统的检查。
当无法成功生成预期的结果时,另一种策略是返回 null
,它是一个表示“无值”的特殊常量。您可以返回 null
代替任何类型的值。稍后在可空类型中,我们将讨论 null
如何影响结果表达式的类型。
Kotlin 标准库中包含 String.toIntOrNull()
,它会在 String
包含整数时执行转换,或者在无法转换时生成 null
——null
是一种简单的指示失败的方式:
// Exceptions/IntroducingNull.kt
import atomictest.eq
fun main() {
"1$".toIntOrNull() eq null
}
假设我们要计算一段时间内的平均收入:
// Exceptions/AverageIncome.kt
package firstversion
import atomictest.*
fun averageIncome(income: Int, months: Int) =
income / months
fun main() {
averageIncome(3300, 3) eq 1100
capture {
averageIncome(5000, 0)
} eq "ArithmeticException: / by zero"
}
如果 months
为零,则 averageIncome()
中的除法会抛出一个 ArithmeticException
。不幸的是,这不告诉我们有关为什么发生错误的任何信息,分母表示什么以及它是否可以合法地为零。这显然是代码中的错误——averageIncome()
应该以一种可以防止除以零错误的方式处理 months
为 0
。
让我们修改 averageIncome()
以提供有关问题来源的更多信息。如果 months
为零,则无法返回常规整数值作为结果。一种策略是返回 null
:
// Exceptions/AverageIncomeWithNull.kt
package withnull
import atomictest.eq
fun averageIncome(income: Int, months: Int) =
if (months == 0)
null
else
income / months
fun main() {
averageIncome(3300, 3) eq 1100
averageIncome(5000, 0) eq null
}
如果一个函数可以返回 null
,Kotlin 要求您在使用结果之前检查它(这在可空类型中有涉及)。即使您只想向用户显示输出,也最好说“尚未经过完整月份”,而不是“您在该期间的平均收入为:null”。
而不是使用错误参数执行 averageIncome()
,您可以抛出异常——跳出并强制程序的其他部分来处理问题。您可以允许默认的 ArithmeticException
,但通常更有用的是抛出一个带有详细错误消息的特定异常。当您的应用程序在生产中运行了几年后,由于一个新功能调用 averageIncome()
而没有正确检查参数,应用程序突然抛出异常,您会对该消息感激不已:
// Exceptions/AverageIncomeWithException.kt
package properexception
import atomictest.*
fun averageIncome(income: Int, months: Int) =
if (months == 0)
throw IllegalArgumentException( // [1]
"Months can't be zero")
else
income / months
fun main() {
averageIncome(3300, 3) eq 1100
capture {
averageIncome(5000, 0)
} eq "IllegalArgumentException: " +
"Months can't be zero"
}
- [1] 在抛出异常时,
throw
关键字后跟要抛出的异常,以及它可能需要的任何参数。在这里,我们使用了标准的异常类IllegalArgumentException
。
您的目标是生成尽可能有用的消息,以简化将来对您的应用程序的支持。稍后您将学会定义自己的异常类型,并使其特定于您的情况。
练习和解答可以在 www.AtomicKotlin.com 找到。