跳至主要內容

详解续体(Continuation)

guodongAndroid大约 6 分钟

前言

是否拥有一等公民续体是区分标准函与可挂起函数的关键。但续体什么呢?在程序每次调用函数时,程序都会把函数添加到调用栈中。这个调用栈是所有函数共有的,按函数调用顺序排列的,栈中的函数都保存在内存中,且都尚未完成。续体操作这个执行流程进而帮助处理调用栈。

您已经了解到续体是一个回调,但在非常低的系统级别实现。更准确的解释是:它是程序控制状态的抽象包装器。它有办法控制程序如何和何时进一步执行,以及它的结果是什么——一个异常或一个值。

当函数执行完成,程序将其从调用栈中取出,并继续执行下一个函数。问题是系统如何知道每个函数执行后返回的位置信息?此信息保存在上述续体中。

每个续体都包含一些关于调用函数的上下文的信息。像局部变量一样,函数传递的参数,调用它的线程等等。当一个函数结束时,通过使用这些信息,系统可以简单地依靠续体来告诉它需要到哪里。

试着从函数调用到结束看看一个函数和一个续体的生命周期是什么。

调用栈

当一个程序第一次启动时,它的调用堆栈只有一个入口——初始函数,通常称为 main。这是因为在其中还没有调用其他函数。初始函数很重要,因为当程序结束时,它会回调 main 函数的续体,从而完成程序,并通知系统将其从内存中释放。

随着程序的运行,它会调用其他函数,将它们添加到调用栈中。

Call stack with continuation

所以如果您有这段代码fun main() {},程序级续体的生命周期包含在主函数的括号内。但是当另一个函数调用时,系统要做的第一件事就是为新函数创建一个新的续体。系统会为新续体添加一些信息,比如它的父函数是谁以及它的续体对象是谁——在本例中是 main 函数。同时它也会携带有关调用函数的代码行、使用哪些参数以及它的返回类型应该是什么。

看看以下代码片段会发生什么:

fun main() {
    val numbers = listOf(1, 2, 5)
}
  • 系统会创建一个续体,它将存在于 listOf 中。
  • 首先,它知道它是在 main 的第一行被调用的,所以它可以在完成时返回到代码中的适当位置。
  • 接下来,它知道它的父函数是 main。这意味着 listOf 会完成这个程序,并将调用一直传播到初始续体。例如,这可能在发生异常时发生。最后,它知道传递给 listOf 的参数是一个可变参数数组,其值为 1、2、5,并且在函数完成时,我们应该得到一个 List<Int>
  • 通过这些信息,它将函数执行和生命周期从调用点引导至返回语句。

在更深层次上,这就像声明一个局部变量,使用指向该变量的指针调用初始化函数,然后在其他地方设置该值——在 listOf 中,然后使用 goto 语句返回初始化函数调用后的一行,此时已有返回的变量以供后续使用。

另一个可以用来解释续体的例子是电子游戏。在大多数电子游戏中,您都有称为检查点的东西。当您进行冒险并探索时,这就像调用一个函数。您必须走一段距离并完成一组较小的任务才能继续。

当您完成上诉任务后,您可以返回检查点并完成冒险。 另一方面,如果发生了不好的事情——您在游戏中的任务失败了,这类似于在计算中抛出异常。您始终可以重新加载游戏并从检查点重新开始。如果将函数包装在 try/catch 块中,则可以实现类似的行为,因为您可以有效地返回检查点并重新开始。

注:此小节的 Continuation 并非 Kotlin 协程中的 Continuation 类,而是上一节描述的系统内置的、不可见的 Continuation

处理续体

getUser 的最新版本中,您将使用 Coroutines API 中的 suspendCoroutine。它是一个顶级函数,允许您创建协程,就像 launch 一样,但它专门用于返回值,而不是启动工作。suspendCoroutine 的另一个不同之处在于它接受一个 lambda 作为参数,它的类型是block: (Continuation<T>) -> Unit。这意味着您可以将 Continuation 作为一等公民处理,随意调用对象上的函数。这允许手动进行控制状态和控制流操作。

Continuations 上可用的函数是 resumeresumeWithresumeWithException 您还可以通过调用 continuation.context 访问 CoroutineContext。稍后您将在 “第 6 章:协程上下文” 中了解上下文。

进一步分析 Continuation,resume 传递了 T 类型的成功值,无论您尝试从协程返回哪种类型。当您认为协程中的条件有效并希望返回到其余代码时,您可以使用它。resumeWithException 接受一个 Throwable,以防出现问题。这允许您完成带有错误的协程,您可以稍后捕获并处理该错误。

这提供了从函数返回值的惊人能力,这可能是异步的,而不知道它们背后是什么。就像 API 就应该是那样一样。您可能在想:但是如果函数没有结束呢?

在这种情况下,您将再次等待一个没有到来的值,从而再次导致另一个停止问题,您的代码被无限期挂起。

Function state

为了解决这个问题,最好是使用续体积极应对。不管怎样,尽量总是返回一个结果,即使它只是一个异常。至少在这种情况下,您的函数将会结束,您也会有一些事情要处理。幸运的是,Continuation 有一个功能可以做到这一点。它被称为 resumeWith,它接受前面提到的 Result 对象。Result 只能是特定时间的两种状态之一。要么是成功,持有您需要的值,要么是失败,持有异常。

它还包含一些实用函数,例如 runCatching,它接收一个它尝试运行的 lambda,在成功情况下以获取某些值。如果出现问题,它会在 try/catch 块的帮助下捕获异常并最终返回失败结果。Continuation 收到 Result 后,将其解包并获取值或异常,以便您自己处理。

每当您使用 suspendCoroutine 或任何其他通过续体来返回值的方式时,强烈建议您强制使用上诉方法,这样您就不会遇到永远不会完成的协程。

注:此小节的 Continuation 并非系统内置的、不可见的 Continuation,而是 Kotlin 协程中的 Continuation 类。