Sequence builder
在其他一些语言中,比如Python或JavaScript,你可以找到使用协程的有限形式的结构:
- async functions异步函数(也称为async/await)
- 生成器函数(通过yield语句生成一系列值的函数)。
我们已经看到了Kotlin中如何使用async,但这将在协程构建器章节中详细解释。Kotlin提供了一个序列生成器(Sequence builder)而不是生成器。
Kotlin序列是类似于集合(如List或Set)的概念,但是它是惰性求值的,意味着下一个元素总是在需要时按需计算。因此,序列具有以下特点:
- 执行最少数量的必需操作;
- 可以是无限的
- 内存使用更高效;
由于这些特性,定义一个生成器来按需计算和“产生”后续元素是非常有意义的。我们可以使用函数 sequence 来定义它。在它的 lambda 表达式内部,我们可以调用 yield 函数来生成此序列的下一个元素。
val seq = sequence {
yield(1)
yield(2)
yield(3)
}
sequence函数是一个小型DSL(领域特定语言)。它的参数是一个带接收者的lambda表达式(suspend SequenceScope<T>.() -> Unit),其中接收者this指的是类型为SequenceScope<T>的对象。它具有像yield这样的函数。当我们调用yield(1)时,它相当于调用this.yield(1),因为this可以隐式使用。如果这是您第一次接触带接收者的lambda表达式,我建议您先学习它们和DSL的创建,因为它们在Kotlin协程中被广泛使用。
这里的关键是每个数字都是按需生成的,而不是提前生成。如果我们在生成器和处理序列的地方都打印一些东西,就可以清楚地观察到这个过程。
val seq = sequence {
println("Generating first")
yield(1)
println("Generating second")
yield(2)
println("Generating third")
yield(3)
println("Done")
}
fun main() {
for (num in seq) {
println("The next number is $num")
}
}
下面是输出
// Generating first
// The next number is 1
// Generating second
// The next number is 2
// Generating third
// The next number is 3
// Done
让我们分析一下它的工作原理。我们请求第一个数字,所以我们进入了生成器。我们打印“生成第一个”,然后生成数字1。然后我们回到循环中并打印生成的数字1,接着发生了关键的事情:执行跳转到我们之前停止的地方以查找另一个数字。这将是不可能的,没有悬挂机制,因为不可能停止一个函数,然后从同一点在未来恢复它。由于悬挂,我们可以在主函数和序列生成器之间跳转,从而实现这一点。
image.png
当我们请求序列中的下一个值时,我们会在生成器中的上一个 yield 后面恢复执行。
让我们手动从序列中获取一些值,以更清晰地看到它是如何工作的。
val seq = sequence {
println("Generating first")
yield(1)
println("Generating second")
yield(2) println("Generating third")
yield(3)
println("Done")
}
fun main() {
val iterator = seq.iterator() println("Starting")
val first = iterator.next() println("First: $first")
val second = iterator.next() println("Second: $second")
// ...
}
输出:
// Starting
// Generating first
// First: 1
// Generating second
// Second: 2
在此处,我们使用了一个迭代器来获取序列中的下一个值。在任何时候,我们都可以再次调用它,以跳转到 builder 函数的中间并生成下一个值。这是否可以在没有协程的情况下实现?或许可以,如果我们为此专门分配一个线程。这样的线程需要被维护,这将是一个巨大的成本。使用协程,它快速而简单。此外,我们可以随意保留此迭代器,因为它几乎没有成本。后面我们将学习此机制在底层如何工作。
真实场景的使用
有几种情况下会使用序列生成器。最典型的情况是生成数学序列,比如斐波那契数列。
image.png
这个构建器也可以用来生成随机数或文本。
image.png
image.png
序列构建器不应该使用除了 yield 操作以外的挂起操作。如果需要获取数据,最好使用 Flow,这将在本书后面解释。Flow 的构建器工作方式类似于序列构建器,但 Flow 具有对其他协程特性的支持。
image.png
现在我们已经了解了协程的基础知识,接下来让我们深入探讨协程的构建器和其他特性。
如何实现挂起?
暂停函数是Kotlin协程的标志。挂起功能是构建所有其他Kotlin协程概念的单个最重要特性。这就是为什么我们在本章的目标是要建立对它的牢固理解。
挂起协程意味着在中途停止它。这类似于停止视频游戏:在检查点保存,关闭游戏,然后您和计算机可以专注于进行不同的事情。然后,当您稍后想要继续时,再次打开游戏,从保存的检查点恢复,因此您可以从之前停止的地方继续播放。这是协程的类比。当它们被暂停时,它们返回Continuation。就像游戏中的保存:我们可以使用它来从停止的地方继续。
请注意,这与线程非常不同,线程不能被保存,只能被阻塞。协程更加强大。挂起时,它不消耗任何资源。协程可以在不同的线程上恢复,并且(至少在理论上)可以序列化,反序列化,然后继续。
Resume
所以让我们看看实际的操作。为此,我们需要一个协程。我们使用协程构建器(如runBlocking或launch)启动协程,稍后我们将介绍它们。虽然还有一种更简单的方法,我们可以使用一个挂起的主函数。
挂起函数是可以暂停协程的函数。这意味着它们必须从协程(或另一个挂起函数)中调用。最后,它们需要有一些东西来挂起。主函数是起点,因此当我们运行它时,Kotlin会在协程中启动它。
image.png
这是一个简单的程序,将打印“Before”和“After”。如果在这两个打印之间挂起会发生什么?为此,我们可以使用 Kotlin 标准库提供的 suspendCoroutine 函数。
image.png
会一直阻塞住, 不会打印出After.
看下面kotlin协程中是如何实现delay函数的:
image.png
Resuming with a value
传递 Unit 到 resume 函数的原因是因为该函数不需要返回值,而只需要通知协程继续执行。同时,我们在 suspendCoroutine 函数中使用了 Unit 作为类型参数,这是因为该函数返回的 Continuation 参数的泛型类型就是 Unit。
image.png
当我们调用suspendCoroutine时,我们可以指定在其continuation中返回的类型。在调用resume时必须使用相同的类型。
image.png
这与游戏类比不太合适。我不知道有什么游戏可以在恢复存档时将东西放进游戏里(除非你作弊并搜索了下一个挑战的解决方案)。然而,这对于协程来说是完全合理的。通常,我们会挂起,因为我们正在等待某些数据,例如来自 API 的网络响应。这是一种常见的场景。您的线程正在运行业务逻辑,直到它达到需要一些数据的点。因此,它要求您的网络库提供它。如果没有协程,那么线程将需要坐在那里等待。这将是一种巨大的浪费,因为线程是昂贵的,特别是如果这是一个重要的线程,比如 Android 上的主线程。使用协程,它只是挂起并给库一个 continuation,指令是“一旦你得到了这些数据,只需将其发送到 resume 函数”。然后线程可以去做其他事情。一旦数据到达,线程将用于从协程挂起的点继续。
为了看到它的实际效果,让我们看一下如何暂停直到接收到某些数据。在下面的示例中,我们使用一个回调函数requestUser,它是在外部实现的。
image.png
调用suspendCoroutine并不方便。我们更愿意有一个挂起函数。我们可以自己提取它。
image.png
目前,许多流行的库(例如 Retrofit 和 Room)已经支持了挂起函数。这就是为什么我们很少需要在挂起函数中使用回调函数的原因。但是,如果你确实需要使用回调函数,我建议使用 suspendCancellableCoroutine(而不是 suspendCoroutine)
image.png
你可能会想知道,如果 API 给我们返回的不是数据,而是一些问题会发生什么。如果服务已经停止工作或者响应一个错误,我们不能返回数据,而是应该从暂停协程的地方抛出一个异常。这时我们需要使用异常来恢复。
Resume with an exception
每个我们调用的函数可能会返回一些值或抛出异常。suspendCoroutine 也是如此。当调用 resume 时,它返回作为参数传递的数据。当调用 resumeWithException 时,作为参数传递的异常从挂起点抛出。
image.png
这种机制用于不同类型的问题,例如用于报告网络异常。
image.png
image.png
挂起协程(coroutine),而不是函数(function)
这里需要强调的一点是,我们挂起的是一个协程,而不是一个函数。挂起函数并不是协程,只是可以挂起协程的函数。想象一下,如果我们将一个函数存储在某个变量中,然后在函数调用之后尝试恢复它。
image.png
这没有任何意义。这相当于停止游戏并计划稍后在游戏中恢复。resume将永远不会被调用。
你只会看到“Before”,除非我们在另一个线程或另一个协程上恢复。为了展示这一点,我们可以设置另一个协程在一秒钟后恢复。
image.png
结语
我希望现在您已经清楚地了解了挂起是如何从用户的角度工作的。这是重要的,我们将在整本书中看到它的应用。它也是实用的,现在您可以将回调函数转换为挂起函数。如果您和我一样,喜欢了解事物的运作方式,那么您可能仍然想知道它是如何实现的。如果您对此感到好奇,将在下一章中介绍。如果您不认为需要了解,请跳过它。这不是很实用,它只是揭示了 Kotlin 协程的魔法。
网友评论