大家好,
今天,我们将再次探讨 Kotlin 协程,这可是一个很重要的主题。要想写出完美的代码,需要好好理解 Kotlin 协程的每一个方面;否则,那就像是一种权宜之计。
我们已经多次用到CoroutineContext
,并且在很多地方都见过它。咱们详细聊聊。
- 协程范围会用到协程上下文。
- ViewModelScope, 使用 CoroutineContext。
任务的协程上下文。
- 如果你记得我们之前写过的关于协程内部的文章,那么你就会明白,每个挂起函数的参数中都有一个 Continuation,并且它们都会使用 CoroutineContext。
因此,我认为我们有必要搞清楚 CoroutineContext 到底是个什么东西。协程中有许多地方都用到了 CoroutineContext。
CoroutineContext
接口表示一个元素或一组元素的集合,概念上类似于映射或集合的数据结构。它是一个带有索引的 Element
实例集合,例如 Job
、CoroutineName
和 CoroutineDispatcher
等。每个 CoroutineContext.Element
本身也是一个 CoroutineContext
,允许将多个元素组合起来。这种结构表明 CoroutineContext
实现了组合设计模式。
我们先看一些代码,然后再看看它的内部是如何工作的。
fun main() {
// 创建一个名为 "A name" 的 CoroutineName 实例
val name: CoroutineName = CoroutineName("A name")
// 将 CoroutineName 实例作为 CoroutineContext.Element
val element: CoroutineContext.Element = name
// 使用 element 创建一个 CoroutineContext
val context: CoroutineContext = element
// 创建一个 Job 实例
val job: Job = Job()
// 将 Job 实例作为 CoroutineContext.Element
val jobElement: CoroutineContext.Element = job
// 使用 jobElement 创建一个 CoroutineContext
val jobContext: CoroutineContext = jobElement
// 使用 name 和 job 实例创建一个新的 CoroutineContext 实例 ctx
val ctx: CoroutineContext = name + job
}
CoroutineContext
设计得易于使用。你可以直接传递单个元素(如 Job
或 CoroutineDispatcher
),而无需将它放入集合,因为单个元素本身就已经被视为一个 CoroutineContext
。
如果你需要同时使用多个元素,你只需将它们组合起来。生成的 CoroutineContext
将会包含你组合的所有元素。这使得创建和管理 CoroutineContext
更加简单又灵活。这让你在使用 CoroutineContext
时更加简单灵活。
我们来看看内部的代码,看看它们是怎么加进去的。
解释一下:
- 因为
CoroutineContext
表示一个集合,你可以使用get
方法通过特定的键来获取元素。你也可以用方括号,因为在 Kotlin 中get
是一个操作符,可以这样调用。结果是一个与键关联类型的可空元素。这类似于从映射中获取元素:如果元素在上下文中存在,将返回该元素;如果不存在,则返回null
。 - 同样地,对于加号,你可以选择用“+”或
.plus
,因为加号在这里也是一个操作符。 - 如果你查看代码会发现,如果传入
EmptyCoroutineContext
,将返回原值,这意味着它不会做任何改动。 - 还有一个函数叫
fold
,它被加号操作符在添加新上下文时使用。
折叠:
- 目的:
fold
函数将CoroutineContext
中的所有元素结合成一个单一的结果。 - 它有两个参数:
initial
和operation
。initial
是累积的初始值,而operation
是一个函数,它接收当前的累积值和一个元素作为输入,然后返回一个新的累积值。 - 过程:函数从
initial
值开始。然后将operation
应用于这个初始值和CoroutineContext
中的每个元素,依次从左到右。每次的应用结果都会成为下一个步骤的新累积值。
在协程上下文中查找组件
我们先来看一个例子吧。
fun main() {
val ctx: 协程上下文(CoroutineContext) = 协程名称(CoroutineName)("一个名称")
val 协程名称: 协程名称(CoroutineName)? = ctx[CoroutineName]
// 可以也可以是 ctx.get(CoroutineName)
println(协程名称?.名称(name))
val 作业: 作业(Job)? = ctx[Job]
println(作业) // null
}
输出:
一个名称
null
解释:
- 要找到一个
CoroutineName
,你只需要使用CoroutineName
。这是因为CoroutineName
在 Kotlin 中引用其伴生对象,从而使从上下文中获取CoroutineName
更为方便。 - 我们来看看
CoroutineName
的实现细节。
在 kotlinx.coroutines 库中,通常使用伴生对象作为同名元素的键,这样更容易记住这些名称。
增加一些上下文:
真正让CoroutineContext
变得非常有用的是它能够合并两个上下文。当添加两个具有不同键的元素时,生成的上下文可以同时响应这两个键。就像在映射中,如果你添加了一个与现有元素具有相同键的新元素,那么新元素会替换旧元素一样。
示例 1:
fun main() {
val ctx1: CoroutineContext = CoroutineName("Name1")
println(ctx1[CoroutineName]?.name) // Name1
println(ctx1[Job]?.isActive) // null
val ctx2: CoroutineContext = Job()
println(ctx2[CoroutineName]?.name) // null
println(ctx2[Job]?.isActive) // true,因为这是默认的活动状态
val ctx3 = ctx1 + ctx2
println(ctx3[CoroutineName]?.name) // Name1
println(ctx3[Job]?.isActive) // true
}
示例 2:
fun main() {
val ctx1: CoroutineContext = CoroutineName("Name1")
println(ctx1[CoroutineName]?.name) // Name1
val ctx2: CoroutineContext = CoroutineName("Name2")
println(ctx2[CoroutineName]?.name) // Name2
val ctx3 = ctx1 + ctx2
println(ctx3[CoroutineName]?.name) // Name2
}
当你使用加号来添加一个新的CoroutineName、Job或Context时,你也可以通过调用minusKey函数来移除。
minus
运算符对 CoroutineContext
没有进行重载。我认为这是因为它含义不够明确,正如《Effective Kotlin》中的第 11 项所述,运算符的用法应与其函数名含义一致。
fun main() {
val ctx = 协程名称("Name1") + Job()
println(ctx[协程名称]?.名称) // Name1
println(ctx[Job]?.isActive) // true
val ctx2 = ctx.minusKey(协程名称)
println(ctx2[协程名称]?.名称) // null
println(ctx2[Job]?.isActive) // true
val ctx3 = (ctx + 协程名称("Name2"))
.minusKey(协程名称)
println(ctx3[协程名称]?.名称) // null
println(ctx3[Job]?.isActive) // true
}
注释:
CoroutineName
被翻译为协程名称
。name
被翻译为名称
。- 其余未翻译的部分如
Job
与isActive
保留英文,因为它们是技术术语,在中文中通常直接使用英文。 - 保留了原始的代码注释,将它们翻译为中文以增强理解。
协程的环境与构建工具:
- 协程上下文实际上是一种用来持有和传递数据的方法。这种传递方式体现了父子协程之间的关系。我们可以说子协程继承了父协程的上下文。
- 然而,每个子协程还可以在参数中定义自己特定的上下文。这个特定的上下文会覆盖继承来的上下文。
fun CoroutineScope.log(msg: String) {
val name = coroutineContext[CoroutineName]?.name
println("[$name] $msg")
}
fun main() = runBlocking(CoroutineName("main")) {
log("开始运行") // [main] 开始运行
val v1 = async {
delay(500)
log("异步运行") // [main] 异步运行
42
}
launch(CoroutineName("Launch")) { // 指定的 CoroutineName 有其特定的 CoroutineContext
delay(1000)
log("启动 launch") // [Launch] 启动 launch
}
log("答案是 ${v1.await()},即延迟后返回的值")
// [main] 答案是 42
}
自己创建我们的情境,
- 虽然不常需要自定义协程上下文,我们可以轻松地创建自己的协程上下文。最简单的方法是创建一个实现
CoroutineContext.Element
接口的类。这个类需要一个类型为CoroutineContext.Key<*>
的属性,称为key,用于标识该上下文。通常的做法是将该类的伴生对象作为key。以下是一个简单的协程上下文实现的示例。
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.coroutineContext
class CounterContext(
private val name: String
) : CoroutineContext.Element {
override val key: CoroutineContext.Key<*> = Key
private var nextNumber = 0
fun printNext() {
println("$name: $nextNumber")
nextNumber++
}
companion object Key : CoroutineContext.Key<CounterContext>
}
suspend fun printNext() {
coroutineContext[CounterContext]?.printNext()
}
suspend fun main(): Unit =
withContext(CounterContext("外部计数器")) {
printNext() // 启动外部计数器: 0
launch {
printNext() // 启动外部计数器: 1
launch(CounterContext("内部计数器")) {
printNext() // 启动内部计数器: 0
printNext() // 启动内部计数器: 1
launch {
printNext() // 启动内部计数器: 2
}
}
}
printNext() // 启动外部计数器: 2
}
好的,这节课就到这里吧。
你们玩得怎么样?
尝试一下,如果有任何问题,可以在评论区提问。
在这里关注我,看看有趣的话题
还有 LinkedIn 上可以关注我,一起扩展我们的开发者圈子吧
很快就会和大家分享一些新的超赞内容。
共同学习,写下你的评论
评论加载中...
作者其他优质文章