1. 上一章:第 16 章
  2. 下一章:第 18 章
Kotlin 图解指南 • 第 17 章

处理运行时异常

章节封面图片

在现实生活中,当我们决定做某件事时,我们主要考虑的是成功的体验。例如,如果你开车去朋友家吃晚饭,你可能会在地图网站上查找路线,确保车里有足够的汽油,并在正确的时间出发,以便在晚餐热的时候到达。如果一切按计划进行,你就会准时到达。

然而,事情并不总是按计划进行。例如,如果在途中爆胎,你就必须换上备胎,去修理厂,并买一个新轮胎。等你做完所有这些事情,晚餐可能已经凉了,你甚至可能不得不取消计划。

与现实生活非常相似,当我们的 Kotlin 代码运行时,事情可能不会按计划进行。我们的函数通常代表我们的计划——我们告诉 Kotlin,”一般来说,按照这个计划执行。”如果出了什么问题,我们会做别的事情,作为该计划的异常

因此,当代码中发生意外情况时,我们称之为异常。在本章中,我们将学习如何处理这些异常!

运行时问题

我们已经看到,错误可以在两个不同的时间出现——编译时或运行时。正如 Cecil 在上一章中发现的,编译时的错误非常有用,因为您可以在应用程序的用户遇到它之前修复它。

然而,并非所有问题都能在编译时检测到。例如,这里有一个函数可以将数字(如 3)转换为其序数(如 “third”)。

val ordinals = listOf("zeroth", "first", "second", "third", "fourth", "fifth")
fun ordinal(number: Int) = ordinals.get(number)

看这个函数,很容易看出它只支持最多 5 的数字的序数。然而,Kotlin 编译器无法确定 number 参数是否会在这些范围内。我们可以很容易地用太高的数字调用这个函数。

fun main() {
    val place = ordinal(9) 
}

运行这个 main() 函数会产生以下错误消息。

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 9 out of 
bounds for length 6
    at java.base/java.util.Arrays$ArrayList.get(Arrays.java:4165)
    at MainKt.ordinal(Main.kt:3)
    at MainKt.main(Main.kt:7)
    at MainKt.main(Main.kt)

计划是简单地返回数字的序数,但事情没有按计划进行。将 9 传递给这个函数导致了计划的异常。由于编译器无法阻止我们用超界的参数调用 ordinal(),我们不得不在运行时处理这个问题。

有许多类似的问题无法在编译时检测到。例如:

  • 在编译时,Kotlin 无法知道映射是否包含特定的键。(请参阅清单 9.11)。
  • 在编译时,Kotlin 无法知道当我们向数据库请求数据时会得到什么值。
  • 在编译时,Kotlin 无法知道用户在提示时可能会在键盘上输入什么。我们可以要求用户输入邮政编码,但可能会得到电话号码。

正因如此,我们需要知道如何处理运行时可能出错的事情。要做到这一点,我们首先需要理解调用栈

调用栈

当你的 Kotlin 程序运行时,一个地方的代码通常会调用另一个地方的代码,而另一个地方的代码又可能调用再一个地方的代码。为了演示这一点,让我们添加一个函数来宣布你计划做的任务的序数。我们不把这个函数命名为 announce(),而将其缩写为 annc(),这样在接下来的一些图表中会更合适。

val ordinals = listOf("zeroth", "first", "second", "third", "fourth", "fifth")
fun ordinal(number: Int) = ordinals.get(number)

fun annc(number: Int, task: String): String {
    val ordinal = ordinal(number)
    return "The $ordinal thing I will do is $task."
}

fun main() {
    val first = annc(1, "clean my room")
    // "The first thing I will do is clean my room."
}

这段代码有一个简单的执行路径。从 main() 函数开始……

  1. main() 函数调用 annc() 函数。
  2. annc() 函数然后调用 ordinal() 函数。
  3. ordinal() 函数调用列表的 get() 函数。
  4. get() 函数将其结果返回给 ordinal() 函数。
  5. ordinal() 函数将其结果返回给 annc() 函数。
  6. annc() 函数将其结果返回给 main() 函数。

当然,当 main() 函数结束时,程序成功完成运行。

这有点像我们的程序在爬山。它从 main() 函数内部的地面 level 开始,对于沿途的每个函数调用,它在山上爬得更高。最终,它进入 get() 函数,此时每个函数依次返回其值,程序沿着山往下走,回到 main() 函数中。

Each function calls the next, and each function returns to the previous - much like ascending and descending a mountain.

与 mountain 的图画不同,更简单的表示方式可能是一叠盒子。

调用栈,从左到右,随着程序运行。 main() annc() ordinal() ordinal() ordinal() get() annc() annc() annc() annc() main() main() main() main() main() main() time

上面的图表仍然大致是 mountain 的形状。每个小盒子代表一个函数。当程序的执行进行时(从左到右),每个函数要么调用另一个函数,要么返回一个值给调用它的函数。

当一个函数调用另一个函数时,下一步的盒子堆将在调用它的函数之上包括那个函数。

调用函数会添加到栈顶。 ordinal() calls get() main() ordinal() annc() main() ordinal() get() annc()

当一个函数返回时,下一步的盒子堆将从栈顶移除该函数。

从函数返回会移除栈顶的盒子。 main() ordinal() annc() main() ordinal() get() annc() get() returns to ordinal()

请注意,这些是栈变化的唯一两种方式!盒子永远不能从除栈之外的任何地方添加或移除!在编程中,这个函数调用栈被称为调用栈,有时也简称为。栈中的每个盒子都是一个栈帧

阐明调用栈和栈帧周围的术语。 main() ordinal() annc() stack frame stack frame stack frame call stack

所以,这个图表显示了程序运行时每个步骤的调用栈是什么样子的。

调用栈随时间变化,在每一步。 Step 1 Step 2 Step 3 Step 4 Step 5 Step 6 Step 7 main() annc() ordinal() ordinal() ordinal() get() annc() annc() annc() annc() main() main() main() main() main() main()

我们从清单 17.3中的例子非常简单——我们只有几个函数,每个调用下一个,所以随着时间的推移,调用栈看起来像一座单独的山。然而,通常它看起来更像山脉。例如,让我们在 main() 函数中添加一个对 println() 的调用。

fun main() {
    val first = annc(1, "clean my room")
    println(first)
}
The first thing I will do is clean my room.

通过这个小的改变,调用栈随时间的变化如下:

调用栈随时间变化,在每一步,额外调用了 `println()`。 Step 8 Step 9 Step 1 Step 2 Step 3 Step 4 Step 5 Step 6 Step 7 main() annc() ordinal() ordinal() ordinal() get() annc() annc() annc() annc() main() main() main() main() main() main() main() main() println()

值得一提的是,从技术上讲,println() 函数本身会调用其他函数,所以如果我们完整地绘制所有这些调用,山脉会比上面的更加复杂!

在我们继续之前,最后要注意的是——在清单 17.3中,我们只有函数调用其他函数,但调用栈通常还包括其他东西。例如,当您调用类的构造函数时,该类的属性也会被初始化,当这种情况发生时,它也是调用栈的一部分。类似地,您可能从具有自定义 getter 函数属性获取值。这些也将在调用栈上有自己的帧。

既然我们已经了解了调用栈,这与异常和错误消息有什么关系?

调用栈、异常和错误消息

让我们更新 main() 函数。不是传递数字 1,而是传递数字 9,这将超出 ordinal() 函数的范围。

fun main() {
    val task = annc(9, "clean my room")
    println(task)
}

当运行此程序时,我们看到的不是”The ninth thing I will do is clean my room”,而是得到一个 ArrayIndexOutOfBoundsException,就像在清单 17.2中一样。这是错误消息的再次展示:

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 
Index 9 out of bounds for length 6
    at java.base/java.util.Arrays$ArrayList.get(Arrays.java:4165)
    at MainKt.ordinal(Main.kt:2)
    at MainKt.annc(Main.kt:5)
    at MainKt.main(Main.kt:10)
    at MainKt.main(Main.kt)

让我们检查当此代码运行时,调用栈如何随时间变化。

调用栈随时间变化,异常发生在 `get()` 函数中。 main() annc() ordinal() ordinal() get() annc() annc() main() main() main()

当事情崩溃时,调用栈上有个帧。我们收到的错误消息包括异常发生时调用栈外观的快照。调用栈在特定时间点的快照被称为栈跟踪

Kotlin 错误消息的解剖图。 Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 9 out of bounds for length 6 at java.base/java.util.Arrays$ArrayList.get(Arrays.java:4165) at MainKt.ordinal(Main.kt:2) at MainKt.annc(Main.kt:5) at MainKt.main(Main.kt:10) at MainKt.main(Main.kt) ... happened in this function stack trace This exception...

请注意,不计算栈跟踪的底部行,它显示的内容与上面的盒子堆相同。

展示错误消息中的堆栈跟踪与可视化调用栈之间的相似性。 ordinal() get() annc() main() at java.baseget(Arrays.java:4165) at MainKt.ordinal(Main.kt:2) at MainKt.annc(Main.kt:5) at MainKt.main(Main.kt:10)

对于栈跟踪中的每一帧,我们可以看到函数的名称、该函数所在文件的名称,以及调用下一个函数的代码的行号

如何解释堆栈跟踪的每一行。 at java.base/java.util.Arrays$ArrayList.get(Arrays.java:4165) at MainKt.ordinal(Main.kt:2) at MainKt.annc(Main.kt:5) at MainKt.main(Main.kt:10) at MainKt.main(Main.kt) at MainKt.annc(Main.kt:5) function name filename line number

理解栈跟踪很容易,但让我们一次看一行。

at java.base/java.util.Arrays$ArrayList.get(Arrays.java:4165)

这一行告诉我们,错误发生在名为 get() 的函数中,它位于名为 ArrayList 的类中。这个类属于 Java 的 API,被 Kotlin 的标准库使用,错误发生在第 4165 行。(是的,这有很多行!)

at MainKt.ordinal(Main.kt:2)

接下来,我们可以看到 get() 函数(从前一行)是从我们的 ordinal() 函数调用的,在我称为 Main.kt 的文件的第 2 行。

at MainKt.annc(Main.kt:5)

之后,ordinal()(从前一行)被 annc()Main.kt 的第 5 行调用。

at MainKt.main(Main.kt:10)

annc() 被同一文件的第 10 行的 main() 函数调用。

at MainKt.main(Main.kt)

而这最后一行只是告诉我们一切都是从 Main.kt 文件开始的。

栈跟踪在您试图找出哪里出了问题时很有帮助,但它也向我们展示了可以在哪里添加代码来尝试阻止程序突然终止。

捕获异常

当异常出现时,这有点像一个孩子朝窗户扔棒球。如果没有人接住那个棒球,它就会撞到窗户上。

Baseball player, throwing a baseball into a window.

类似地,在我们的 Kotlin 代码中,当抛出异常时,如果没有人捕获异常,错误消息将被打印出来,程序将崩溃。

A baseball being thrown is much like an exception being thrown in Kotlin - if nobody catches it, there will be a crash.

通常,我们希望程序在遇到异常时崩溃。为了说明原因,让我们将单个任务替换为任务列表,并为每个任务打印一条公告。

fun main() {
    val tasks = listOf(1 to "clean my room", 9 to "take out trash", 5 to "feed the dog")
    tasks.forEach { (number, task) -> 
        println(annc(number, task)) 
    }
}

当运行此程序时,我们将看到以下输出:

The first thing I will do is clean my room.
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 9 out of bounds for length 6
    at java.base/java.util.Arrays$ArrayList.get(Arrays.java:4165)
    at MainKt.ordinal(Main.kt:2)
    at MainKt.annc(Main.kt:5)
    at MainKt.main(Main.kt:11)
    at MainKt.main(Main.kt)

第一个任务被打印到屏幕上,但第二个任务导致异常,所以程序中止了。因为程序停止运行,所以它从未打印出喂狗的任务!如果程序即使在有异常的情况下也能继续运行,那就太好了。

为了让事情继续运行,我们需要捕获那个异常。换句话说,我们需要有人在棒球撞穿窗户之前接住它!

那么,我们如何捕获异常?嗯,我们可以在调用栈的任何帧放置一个棒球运动员。从抛出异常的帧开始,异常将一个接一个地经过每个栈帧,一直到程序开始的 main() 函数。在沿途的每一步,如果那个帧有一个棒球运动员,他或她可以捕获异常,防止窗户破裂。

例如,我们可以在 main() 函数中放置一个捕手,她会在异常撞击窗户之前将其捕获:

Catching the exception baseball prevents a crash.

让我们添加一些代码来捕获异常!为此,我们将使用try 表达式,它看起来像这样:

Kotlin 中 try 表达式的结构剖析。 try { do something exception-prone here } catch (exception: Exception) { handle the exception here } "try" keyword "catch" keyword exception parameter

这个表达式有几个关键部分:

  1. 它以 try 关键字开头。
  2. 之后是 try 块。这是我们放置可能抛出异常的代码的地方。
  3. 之后是 catch 关键字。
  4. 括号内是异常参数,它用我们想要捕获的异常类型的名称和类型声明。我们稍后会详细了解这一点。
  5. 之后是 catch 块。这是我们放置捕获异常时应运行的代码的地方。

首先,让我们把我们的捕手(即 try 表达式)放在 main() 函数中,她可以在异常打破窗户之前捕获它。当我们调用 annc() 时,最终可能会抛出异常。由于该调用可能抛出异常,我们将把它放在 try 块的内部。然后,在 catch 块内部,我们将简单地打印出出了问题。这就是我们最终得到的结果:

fun main() {
    val tasks = listOf(1 to "clean my room", 9 to "take out trash", 3 to "feed the dog")
    tasks.forEach { (number, task) -> 
        try {
            println(annc(number, task)) 
        } catch (exception: Exception) {
            println("Something went wrong!")
        }
    }
}

当我们再次运行它时,在处理”take out trash”时我们仍然会得到异常,但这次异常将被 catch处理。结果,我们看到:

The first thing I will do is clean my room.
Something went wrong!
The fifth thing I will do is feed the dog.

通过这个更改,我们防止了程序因错误消息而崩溃!这意味着我们仍然可以看到关于喂狗的任务!处理完所有三个任务后,程序成功结束。

打印出”Something went wrong!”总比什么都没有好,但当我们捕获异常时,还有更多我们可以做的事情。

异常是对象

异常是一个对象,我们可以像使用任何其他对象一样使用它的函数和属性。在上面的清单 17.7中,我们的 catch 块声明了一个名为 exception 的异常参数,该参数的类型是 Exception

具有 Exception 类型的异常参数。 try { println ( announcement (number, task)) } catch (exception: Exception) { println ( "Something went wrong!" ) }

虽然这里使用了变量名 exception,但大多数开发人员更喜欢使用非常短的变量名来表示异常,例如 ex 或 just e

try {
    println(annc(number, task)) 
} catch (e: Exception) {
    println("Something went wrong!")
}

这个变量在 catch 块内可见,因此您可以根据需要与其交互。例如,每个 Exception 对象都有一个名为 message 的可空属性,因此让我们更新我们的 println() 以包含该消息。

try {
    println(annc(number, task)) 
} catch (e: Exception) {
    println("Something went wrong! ${e.message}")
}

现在当运行此程序时,我们可以看到什么东西出错了,而不仅仅是看到某物出错了——在这种情况下,索引 9 超出了长度为 6 的数组的范围。

The first thing I will do is clean my room.
Something went wrong! Index 9 out of bounds for length 6
The fifth thing I will do is feed the dog.

除了 message 属性外,Exception 对象还包括一个栈跟踪,您可以使用 printStackTrace() 函数打印出来。1

但是,我们并不局限于使用 Exception 类本身。我们可以扩展它,并包含我们想要的任何函数或属性!不过,在这之前,我们需要先学习如何抛出我们自己的异常。

抛出异常

除了捕获异常,我们还可以自己抛出它们。由于异常只是一个对象,我们可以像平常一样实例化它——通过调用其构造函数。我们不必向其构造函数传递任何参数,但给它一条描述问题的消息可能会有帮助。

val exception = Exception("No cleaning allowed on holidays!")

一旦您有了一个异常实例,您可以使用 throw 关键字抛出该异常,所以我们可以这样做:

throw exception

但是,异常的栈跟踪是在异常实例化的点确定的,而不是在抛出的点确定的。因此,我们通常不会像在清单 17.10中那样实例化并存储异常。相反,通常在抛出异常时实例化它,像这样。

throw Exception("No cleaning allowed on holidays!")

让我们更新我们的 annc() 函数,这样如果 task 包含”clean”这个词,我们就会抛出这个异常。

fun annc(number: Int, task: String): String {
    if ("clean" in task) throw Exception("No cleaning allowed on holidays!")
    val ordinal = ordinal(number)
    return "The $ordinal thing I will do is $task."
}

使用来自清单 17.9的 try 表达式运行此程序会产生以下输出:

Something went wrong! No cleaning allowed on holidays!
Something went wrong! Index 9 out of bounds for length 6
The fifth thing I will do is feed the dog.

我们的 try 表达式现在正在处理至少两种不同情况的异常:

  1. 当任务包含”clean”这个词时抛出的异常。
  2. 当索引超出范围时抛出的异常。

目前,我们以相同方式处理这两种异常,但我们可能希望不同地处理每种情况。为了区分这两种不同类型的异常,我们可以确保它们有不同的类型

异常类型

虽然我们可以像在清单 17.13中那样直接实例化 Exception 类,但为我们抛出的每个独特异常类别扩展它通常是有帮助的。例如,我们可以创建一个专用于节假日的 Exception 子类,当我们不被允许清洁时。

class HolidayException(val task: String) : Exception("'$task' is not allowed on holidays")

现在我们可以抛出 HolidayException 而不是一般的 Exception

fun annc(number: Int, task: String): String {
    if ("clean" in task) throw HolidayException(task)
    // ...
}

通过此更改,此程序现在处理两种异常:

  • ArrayIndexOutOfBoundsException,可能从列表的get()函数抛出。
  • HolidayException,可能从我们的annc()函数抛出。

这些每一个都是 Exception 类的子类,尽管 ArrayIndexOutOfBoundsException 经过了一些中间类。下面是一个 UML 类图,显示这些类是如何关联的。

UML 类图,展示了 Kotlin 中异常类的层次结构,包括 Exception、RuntimeException、IndexOutOfBoundsException、ArrayIndexOutOfBoundsException 和自定义的 HolidayException。 HolidayException Exception RuntimeException ArrayIndexOutOfBoundsException IndexOutOfBoundsException

让我们再次看看我们的 try 表达式。

try {
    println(annc(number, task)) 
} catch (e: Exception) {
    println("Something went wrong! ${e.message}")
}

异常参数的类型告诉 Kotlin 哪些种类的异常应该在这个 catch 块中处理。这就像告诉棒球运动员只接某种颜色的球,而忽略其他的。

清单 17.16中,类型是 Exception,所以这个 catch 块将处理任何类型为 Exception 的异常,包括其任何子类型。因为 ArrayIndexOutOfBoundsExceptionHolidayException 都是 Exception 的子类型,所以这段代码将捕获两者这些异常。

但是,我们可以将此类型更改为更具体的内容。例如,让我们将其更改为 HolidayException

try {
    println(annc(number, task))
} catch (e: HolidayException) {
    println("Something went wrong! ${e.message}")
}

进行此更改后,我们可以运行代码,并在输出中看到 try 表达式捕获并处理了 HolidayException,但它没有捕获第二个任务引起的 ArrayIndexOutOfBoundsException

运行代码后的输出,指示处理了哪些异常。 Something went wrong! 'clean my room' is not allowed on holidays Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 9 out of bounds for length 6 (stack trace follows) this exception was handled this exception was not handled

所以,异常参数的类型决定哪些异常被捕获。请记住,catch 块将处理指定的类型,包括其任何子类型。

用于 Exception 的 catch 块将包含 Exception 及其所有子类型。 HolidayException Exception RuntimeException ArrayIndexOutOfBoundsException IndexOutOfBoundsException ... will catch all of these exception types: try { } catch (e: Exception) { } This code... 用于 HolidayException 的 catch 块仅包含 HolidayException,因为它没有子类型。 HolidayException Exception RuntimeException ArrayIndexOutOfBoundsException IndexOutOfBoundsException ... will catch just this exception type: try { } catch (e: HolidayException) { } This code... 用于 RuntimeException 的 catch 块将包含 RuntimeException 及其所有子类型。 HolidayException Exception RuntimeException ArrayIndexOutOfBoundsException IndexOutOfBoundsException ... will catch these exception types: try { } catch (e: RuntimeException) { } This code...

任何 catch 块没有捕获的异常类型将简单地继续到下一个栈帧,在那里另一个 try 表达式可能有机会捕获并处理它。与往常一样,如果没有人捕获异常,它就会撞穿窗户。

If no catch block catches the exception, then there will be a crash.

不同地处理多种异常类型

有时您可能希望在单个 try 表达式中处理多种类型的异常,但对每个异常进行不同的操作。例如,不是捕获 HolidayException 而忽略 ArrayIndexOutOfBoundsException,您可能希望 main() 函数同时处理两者,但在每种情况下打印不同的内容。

为此,我们可以根据需要简单地附加额外的 catch 块,像这样。

try {
    println(annc(number, task))
} catch (e: HolidayException) {
    println("It's a holiday! I'm not going to ${e.task} today!")
} catch (e: ArrayIndexOutOfBoundsException) {
    println("I can't count that high!")
}
It's a holiday! I'm not going to clean my room today!
I can't count that high!
The fifth thing I will do is feed the dog.

请注意,当有多个 catch 块时,第一个具有匹配异常类型的块获胜——任何后续块都将被忽略,即使其异常参数的类型匹配。正因如此,考虑 catch 块的顺序是个好主意。例如,我们可以在最顶部添加一个 Exception 的 catch 块:

try {
    println(annc(number, task))
} catch (e: Exception) {
    println("I wasn't expecting this!")
} catch (e: HolidayException) {
    println("It's a holiday! I'm not going to ${e.task} today!")
} catch (e: ArrayIndexOutOfBoundsException) {
    println("I can't count that high!")
}

但是,如果我们这样做,后面两个 catch 块将是无意义的。因为 HolidayExceptionArrayIndexOutOfBoundsExceptionException子类,它们将始终只匹配第一个块!

I wasn't expecting this!
I wasn't expecting this!
The fifth thing I will do is feed the dog.

所以,相反,如果一个 try 表达式要同时包含异常类其一个或多个子类的情况,请将子类放在其相应超类之上,像这样:

try {
    println(annc(number, task))
} catch (e: HolidayException) {
    println("It's a holiday! I'm not going to ${e.task} today!")
} catch (e: ArrayIndexOutOfBoundsException) {
    println("I can't count that high!")
} catch (e: Exception) {
    println("I wasn't expecting this!")
}

通过此更改,HolidayException 将在第一个 catch 块中处理,ArrayIndexOutOfBoundsException 将在第二个 catch 块中处理,而任何其他类型的异常将在第三个 catch 块中处理。

求值 Try 表达式

在整个章节中,我们将 try-catch 称为表达式。您可能还记得,语句和表达式之间的区别在于表达式可以被求值——换句话说,它可以归约为一个值。由于 try 表达式确实是表达式,我们可以将它们赋值给变量、传递给函数,或从函数返回。

例如,目前,我们在 main() 函数中的 try 表达式在每种情况下都调用 println()

try {
    println(annc(number, task))
} catch (e: HolidayException) {
    println("It's a holiday! I'm not going to ${e.task} today!")
} catch (e: ArrayIndexOutOfBoundsException) {
    println("I can't count that high!")
}

不是在各处调用 println(),我们可以简单地让这个 try 表达式求值为一个 String。为此,我们只需移除每个 println(),并将结果赋值给一个变量。然后我们可以打印该变量。

val words: String = try {
    annc(number, task)
} catch (e: HolidayException) {
    "It's a holiday! I'm not going to ${e.task} today!"
} catch (e: ArrayIndexOutOfBoundsException) {
    "I can't count that high!"
}

println(words)

这正如您所期望的那样工作:

  • 如果没有异常,annc() 的结果将被赋值给 words 变量。
  • 如果有 HolidayException,则该块中的字符串将被赋值给 words。(”今天是假日!……”)
  • 如果有 ArrayIndexOutOfBoundsException,则”我数不到那么高!”将被赋值给 words
  • 如果有任何其他异常,它将不会在这里被捕获。

Try-Catch-Finally

从前,有一个友好的邻居叫 Tara。她为拥有一片郁郁葱葱、绿色、修整整齐的草坪而自豪。每周两次,她会打开室外水龙头开始浇灌草坪。有一天,打开水龙头后,她注意到洒水器没有按应有的方式旋转。

意识到洒水器坏了,她走进屋子,从网上商店订了一个新的。然后,她继续处理那天需要做的其他事情。不幸的是,到了一天结束时,她的草坪被水淹了,因为她从未关掉水龙头!

就像 Tara 一样,有时您需要确保完成一项任务,即使出了问题!以下代码模拟了 Tara 的经历:

try {
    faucet.turnOn()
    watch(sprinkler) // SprinklerBrokenException is thrown here
    faucet.turnOff() // This never runs!
} catch (e: SprinklerBrokenException) {
    store.orderNewSprinkler()
}

这段代码看起来合理,但有一个问题——如果 watch() 函数抛出 SprinklerBrokenException,则 faucet.turnOff() 永远不会运行!

为了帮助处理这种情况,您可以在 try 表达式的末尾包含一个名为 finally 的块。finally 块将在表达式的其余部分被处理后运行,无论是否抛出异常。它是这样的:

try {
    faucet.turnOn()
    watch(sprinkler) // SprinklerBrokenException is thrown here
} catch (e: SprinklerBrokenException) {
    store.orderNewSprinkler()
} finally {
    faucet.turnOff() // This will run, even when the sprinkler breaks!
}

通过此代码:

  • 如果没有抛出异常,则 faucet.turnOff() 将在 watch(sprinkler) 完成后运行。
  • 如果抛出 SprinklerBrokenException,则 faucet.turnOff() 将在 store.orderNewSprinkler() 完成后运行。
  • 如果抛出任何其他类型的异常,则 faucet.turnOff() 将在异常向栈的起点传播之前运行。

finally 块通常对于需要关闭的资源最有帮助。例如,如果您的程序要读取计算机上的文件,您会希望确保在处理完毕后将其关闭,即使在处理过程中抛出异常。

请注意,即使没有任何 catch 块,您也可以将 finallytry 结合使用!例如,您可能希望异常由更接近栈开头的另一个 try 表达式处理,但仍然需要确保关闭水龙头。

try {
    faucet.turnOn()
    watch(sprinkler)
} finally {
    faucet.turnOff()
}

至少,try 表达式必须有一个 catch 块或一个 finally 块。

Try 表达式有点冗长。它们往往在屏幕上占用大量空间,并导致难以遵循的执行路径,因为并不总是能明显地看出异常将在哪里被处理。Kotlin 包含了另一种异常处理方法,这也值得考虑。

异常处理的函数式方法

Kotlin 的标准库包含一个名为 runCatching() 的函数。在内部,它使用 try 表达式,但通过使用这个函数,我们通常可以避免在代码中手动编写 try 表达式。使用这个函数很容易。首先,只需用可能抛出异常的代码调用它,并将其结果赋值给一个变量,像这样:

fun main() {
    val tasks = listOf(1 to "clean my room", 9 to "take out trash", 5 to "feed the dog")
    tasks.forEach { (number, task) -> 
        val result = runCatching { annc(number, task) } 
    }
}

runCatching()将返回一个Result对象,其中包含两者之一——要么是成功的结果,要么是一个异常。在上述代码中,这意味着……

  1. 如果 annc() 成功完成,则它将包含 annc() 返回的结果。
  2. 如果 annc() 抛出异常,则它将包含该异常。

Result 对象上有几个函数,可用于处理结果或异常。处理它的一个简单方法是使用其 getOrDefault() 函数。例如,以下代码的作用与清单 17.8相同:

val result = runCatching { annc(number, task) }
val text = result.getOrDefault("Something went wrong!")
println(text)

在这段代码中,如果 annc() 成功完成,则其结果将被赋值给 text 变量。否则字符串”Something went wrong!”将被赋值给它。这在很多情况下都能正常工作,但如果我们需要的默认值是基于异常的属性——例如,如果我们想包含异常的 message——那么我们需要别的东西。

对于这种情况,我们可以使用 getOrElse()。这个函数接受一个 lambda,您可以在其中操作异常对象。2 例如,下面的代码与清单 17.9的作用相同。

val result = runCatching { annc(number, task) }
val text = result.getOrElse { "Something went wrong! ${it.message}" }
println(text)

Result 类包含许多其他函数,可用于转换其值或异常。这些函数可以链接在一起,就像集合操作链一样。

runCatching()Result 可以使某些代码比等效的 try 表达式更容易阅读。但是,如果我们自己编写 try 表达式,我们会获得更多控制——我们可以捕获特定类型的异常,并在需要时添加 finally 块。

此外,来自函数式编程背景的开发人员通常需要更高级的功能,例如仅捕获某些类型异常的能力或累积多个异常的能力。如果这是您,您可能需要查看 Kotlin 的 Arrow 函数式编程库。

总结

您在本章中出色地完成了关于异常的学习,包括:

在下一章中,我们将最终仔细研究泛型!到时候见!


  1. 当您的 Kotlin 代码针对 Java 虚拟机或原生平台时,此函数可用,但目前针对 JavaScript 时不可用。 ↩︎

  2. 此对象的类型实际上不是 Exception,而是 Throwable。请参阅上面的可抛出对象和错误部分,了解它们如何相互关联的更多信息。 ↩︎