接着上一节,我们来看看使用 TLV 时可能遇到的一些更复杂的场景。
TLV 的嵌套
首先,设置 TLV 的 withValue
是可以嵌套的,嵌套之后的规则和一般变量名的覆盖规则是相同的:内层 TLV 的值会替换掉外层 TLV。因此,下面这个例子中,内层 Work.workID
的值是 BX10,回到外层之后,就会变回 BX11:
Task(priority: .userInitiated) {
Work.$workID.withValue("BX11") {
syncPrintWorkID(tag: "Outer") // BX11
Work.$workID.withValue("BX10") {
syncPrintWorkID(tag: "Inner") // BX10
}
syncPrintWorkID(tag: "Outer") // BX11
}
}
当然,在 withValue
的 closure 里也存在着一种例外的情况:
Work.$workID.withValue("BX10") {
syncPrintWorkID(tag: "Inner") // BX10
Task.detached(priority: .userInitiated) {
syncPrintWorkID(tag: "Detached")
}
}
之前我们说过,detached task 并不会继承创建它的时候的上下文,因此,它当然也就不会继承属于这个上下文里的 TLV。尽管我们通过 withValue
把 Work.workID
设置成了 BX10,在 detached task 中的 Work.workID
的值仍旧是 no-work-id。
TLV 在函数调用过程中的可见性
第二个场景,TLV 的值是会随着函数调用栈一直传递下去的:
func inner() -> String? {
syncPrintWorkID(tag: "inner") // BX11
return Work.workID
}
func middle() async -> String? {
syncPrintWorkID(tag: "middle") // BX11
return inner()
}
func outer() async -> String? {
await Work.$workID.withValue("BX11") {
syncPrintWorkID(tag: "outer")
let ret = await middle()
return ret
}
}
例如上面这个例子,我们在 outer
中把 Work.workID
设置成 BX11。在接下来的调用栈里,middle
和 inner
中读取 Work.workID
的值得到的都是 BX11。因此,当我们看到直接读取 TLV 的时候,并不一定读到的就是默认值。
TLV 在子任务中的继承
第三个场景,就是子任务继承父任务 TLV 的情况了。当我们在一个任务上下文环境中使用 withValue
设置了 TLV,在 withValue
的 closure 中继续创建子任务的时候,子任务中读到的 Work.workID
也会是 BX11。也就是说,父子任务的 TLV 不是独立的,它会继承到子任务。当然,子任务也可以在它自己的作用域里,覆盖掉父任务的值。
Work.$workID.withValue("BX11") {
Task(priority: .userInitiated) {
syncPrintWorkID(tag: "SubTask")
}
}
这个例子应用到异步版本的 withValue
中,也同样适用。通过 group.addTask
创建的任务中,Work.workID
的值也是 BX11:
await Work.$workID.withValue("BX11") {
await withTaskGroup(of: String?.self) { group -> String? in
group.addTask {
syncPrintWorkID(tag: "SubTask")
return Work.workID
}
return await group.next()!
}
}
但要注意的是,我们只能在 withValue
的 closure 中创建任务群组。如果在 withTaskGroup
中设置 TLV,则会触发运行时错误。在当前的 Swift 5.5 版本里,这是一个不被支持的编程方式:
await withTaskGroup(of: Void.self) { group in
Work.$workID.withValue("BX11") { // Runtime Exception
syncPrintWorkID(tag: "Do not do this")
group.addTask {}
}
}
没有任务上下文环境的情况
说到这里,我们所有的例子,都是带有任务上下文环境的。如果我们在没有任务上下文环境的情况下访问或设置 TLV 会如何呢:
@main
struct MyApp {
static func main() {
syncPrintWorkID(tag: "Outer") // BX11
Work.$workID.withValue("BX10") {
syncPrintWorkID(tag: "Inner") // BX10
}
syncPrintWorkID(tag: "Outer") // BX11
}
}
在上面这个例子中,Swift 会借用线程局部存储来模拟 TLV 需要的上下文环境。我们刚才说过的作用域以及 API 的用法,在传统的同步代码环境下,行为和之前是一一样的。
What's next?
至此,关于 TLV 的基础内容,我们就说的差不多了。下一节,我们来说一个相比 TLV 更为常用的 Swift 5.5 新特性:async let
。