
网罗开发 (小红书、快手、视频号同名)
大家好,我是 展菲,目前在上市企业从事人工智能项目研发管理工作,平时热衷于分享各种编程领域的软硬技能知识以及前沿技术,包括iOS、前端、Harmony OS、Java、Python等方向。在移动端开发、鸿蒙开发、物联网、嵌入式、云原生、开源等领域有深厚造诣。
图书作者:《ESP32-C3 物联网工程开发实战》
图书作者:《SwiftUI 入门,进阶与实战》
超级个体:COC上海社区主理人
特约讲师:大学讲师,谷歌亚马逊分享嘉宾
科技博主:华为HDE/HDG
我的博客内容涵盖广泛,主要分享技术教程、Bug解决方案、开发工具使用、前沿科技资讯、产品评测与使用体验 。我特别关注云服务产品评测、AI 产品对比、开发板性能测试以及技术报告,同时也会提供产品优缺点分析、横向对比,并分享技术沙龙与行业大会的参会体验。我的目标是为读者提供有深度、有实用价值的技术洞察与分析。
展菲:您的前沿技术领航员
👋 大家好,我是展菲!
📱 全网搜索"展菲",即可纵览我在各大平台的知识足迹。
📣 公众号"Swift社区",每周定时推送干货满满的技术长文,从新兴框架的剖析到运维实战的复盘,助您技术进阶之路畅通无阻。
💬 微信端添加好友"fzhanfei",与我直接交流,不管是项目瓶颈的求助,还是行业趋势的探讨,随时畅所欲言。
📅 最新动态:2025 年 3 月 17 日
快来加入技术社区,一起挖掘技术的无限潜能,携手迈向数字化新征程!
文章目录
-
- 前言
- [Task Local Values 是什么?解决的到底是什么问题?](#Task Local Values 是什么?解决的到底是什么问题?)
- [一个最基础的 Task Local 示例](#一个最基础的 Task Local 示例)
- [在 async 场景中使用 Task Local](#在 async 场景中使用 Task Local)
-
- [1. Task Local 是只读的](#1. Task Local 是只读的)
- [2. withValue 的作用范围是整个 async 调用链](#2. withValue 的作用范围是整个 async 调用链)
- 这个能力在真实项目里有什么用?
- [用 Task Local 构建一个依赖容器](#用 Task Local 构建一个依赖容器)
- 定义依赖容器
- 生产环境依赖
- [Mock 版本依赖](#Mock 版本依赖)
- [用 Task Local 管理"当前依赖"](#用 Task Local 管理“当前依赖”)
- [在测试中使用 Mock 依赖](#在测试中使用 Mock 依赖)
- 这种方式适合你吗?
- 总结
前言
Swift 并发体系这两年一直在快速进化,除了我们熟悉的 async/await、TaskGroup、Actor 之外,其实还悄悄加入了一个非常有意思、但讨论不算多的能力:Task Local Values。
乍一看,它好像只是一个"任务级别的全局变量",但一旦你理解了它的设计初衷,就会发现它非常适合用来做一些以前很难优雅实现的事情,比如:
- 在并发任务中传递上下文信息(requestId、traceId)
- 做统一日志、埋点
- 甚至,用来构建一个隐式的依赖注入容器
这篇文章会从最基础的 Task Local Values 讲起,然后一步步带你实现一个基于 Task Local 的依赖容器,并结合真实业务和测试场景分析它到底适不适合你。
Task Local Values 是什么?解决的到底是什么问题?
一句话概括:
Task Local Values 是一种"随 Task 传播的共享状态",对子任务自动可见,而且同时支持同步和异步访问。
它解决的是并发环境下一个非常现实的问题:
在一堆 async / await、子任务、任务组中,我怎么优雅地把"上下文信息"一路传下去?
比如下面这些场景你一定遇到过:
- 一个网络请求需要生成 requestId,然后在多个并发子任务里都要用
- 日志系统需要在任何 async 方法中都能拿到当前请求的标识
- 测试时希望"偷偷"替换某些依赖,但不想层层传参数
以前你可能会选择:
- 手动把参数一层层传下去(非常烦)
- 用全局变量(线程不安全)
- 用 ThreadLocal(Swift 没有)
Task Local Values 就是 Apple 给出的标准答案。
一个最基础的 Task Local 示例
我们先从一个最简单、也最经典的场景开始:请求上下文传递。
swift
struct Request: Identifiable {
let id = UUID()
}
这里我们定义了一个 Request,内部只有一个 UUID,模拟真实世界里的 requestId。
接下来是关键代码:
swift
extension Request {
@TaskLocal static var current = Request()
}
这行代码做了几件非常重要的事情:
@TaskLocal只能用在 static 属性 上- 它定义了一个"当前 Task 可见的共享值"
- 必须有默认值(或者定义成 Optional)
你可以把 Request.current 理解成:
当前并发任务树中,大家默认能看到的那个 Request
这个设计和 SwiftUI 的 Environment 非常像,只不过作用域从"视图树"变成了"任务树"。
在 async 场景中使用 Task Local
下面我们来看一个稍微真实一点的例子。
swift
func fetchData() async throws -> Data? {
let newRequest = Request()
return try await Request.$current.withValue(newRequest) {
try await withThrowingTaskGroup(of: Data.self) { group in
group.addTask {
let url = URL(
string: "https://example.com/api/\(Request.current.id.uuidString)"
)!
let (data, _) = try await URLSession.shared.data(from: url)
return data
}
group.addTask {
// 在任何子任务里都可以直接访问 Request.current
print("Current request id:", Request.current.id)
return Data()
}
for try await data in group {
return data
}
}
}
}
这里有几个关键点一定要注意:
1. Task Local 是只读的
你不能直接写:
swift
Request.current = newRequest // ❌ 不允许
唯一正确的方式是使用:
swift
Request.$current.withValue(newValue) {
// 在这个闭包作用域内生效
}
2. withValue 的作用范围是整个 async 调用链
只要是在这个 closure 里面启动的 async 操作、子任务、TaskGroup,都能自动拿到这个值。
这点非常关键,也是 Task Local 的核心价值。
这个能力在真实项目里有什么用?
到这里你可能会想:
好像挺酷,但我真的会用到吗?
其实你可能已经在用类似的东西,只是方式更笨一点。
常见应用场景包括:
- 请求级日志上下文(requestId、userId)
- 性能追踪、链路追踪
- A/B 实验参数
- 灰度发布标识
而这些数据都有一个共同特点:
- 不适合写成全局变量
- 不想每个函数都传参数
- 生命周期和一次请求/任务绑定
Task Local 正好卡在这个位置。
用 Task Local 构建一个依赖容器
接下来进入这篇文章最有意思的部分:用 Task Local 做依赖注入。
为什么要这么做?
在 Swift 项目里,依赖注入通常有几种方式:
- 构造函数注入(很啰嗦)
- 全局单例(测试困难)
- Service Locator(容易失控)
Task Local 提供了一种折中的思路:
在一个 async 任务作用域里,隐式切换依赖实现
定义依赖容器
我们先定义一个依赖集合:
swift
struct Dependencies {
let fetchStatistics: (DateInterval) async throws -> [HKStatistics]
}
这里为了简化,只放了一个方法。真实项目中你可能会有:
- 网络请求
- 数据库
- 本地缓存
- Feature flag
- 权限判断
生产环境依赖
swift
extension Dependencies {
static var production: Dependencies {
let store = HKHealthStore()
return .init(
fetchStatistics: { interval in
let query = HKStatisticsCollectionQueryDescriptor(
predicate: .quantitySample(type: HKQuantityType(.bodyMass)),
options: .discreteAverage,
anchorDate: interval.start,
intervalComponents: DateComponents(day: 1)
)
return try await query
.result(for: store)
.statistics()
}
)
}
}
这是一个真实的生产实现,会调用系统 API。
Mock 版本依赖
swift
extension Dependencies {
static var mock: Dependencies {
let mockedStatistics: [HKStatistics] = [
// 构造假的数据
]
return .init(
fetchStatistics: { _ in mockedStatistics }
)
}
}
Mock 版本不会访问系统、不依赖权限,非常适合测试。
用 Task Local 管理"当前依赖"
swift
extension Dependencies {
@TaskLocal static var active: Dependencies = .production
}
这一行是整个设计的核心。
它意味着:
- 默认情况下,所有代码用的都是 production
- 但在某个 Task 作用域里,你可以悄悄换成 mock
在测试中使用 Mock 依赖
swift
@Test func verifySomething() async throws {
await Dependencies.$active.withValue(.mock) {
let interval: DateInterval = // 构造测试区间
let statistics = try await Dependencies
.active
.fetchStatistics(interval)
#expect(statistics.count == 1)
}
}
这里有几个非常爽的点:
- 不需要改任何业务代码
- 不需要传 mock 参数
- 不需要全局开关
- 并发安全
测试代码只负责"在这个 Task 里,用 mock 版本"。
这种方式适合你吗?
说实话,这不是银弹。
适合的场景
- 以 async/await 为主的现代 Swift 项目
- 强调并发安全
- 想要轻量 DI,而不是完整框架
- 测试中需要大量 mock
不适合的场景
- 同步代码占比极高
- 依赖关系非常复杂、层级很深
- 团队对隐式依赖不熟悉(可读性风险)
总结
Task Local Values 表面看是并发的小功能,但本质上提供了一种新的"上下文传播模型"。
当你用它来做:
- 请求上下文
- 日志追踪
- 依赖注入
你会发现它比传统方案:
- 更安全
- 更简洁
- 更贴合 Swift Concurrency 的设计哲学
如果你正在构建一个以 async/await 为核心的新项目,非常值得认真考虑这种模式。