Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

异常

词语“异常”在这里的意义与短语“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() 应该以一种可以防止除以零错误的方式处理 months0

让我们修改 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 找到。