ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

在 Composable Architecture 中采用 Swift 并发:编写安全并发 Effect 的完整指南

在 Composable Architecture 中采用 Swift 并发:编写安全并发 Effect 的完整指南 前端移动开发【免费下载链接】swift-composable-architectureA library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind.项目地址https://gitcode.com/GitHub_Trending/sw/swift-composable-architecture点击查看免费下载导读Swift 5.6 开始会对并发上下文中可能不安全的类型与函数用法发出警告而这些警告在 Swift 6 中将大多升级为编译错误。对使用 TCAswift-composable-architecture的开发者来说这意味着必须学会向编译器证明自己的类型可以安全地跨并发域传递。本文以官方文档 SwiftConcurrency.md 为主线系统讲解如何通过Effect.run编写安全的异步效果Effect重点解决两个最常见、也最容易触发 Swift 6 编译错误的场景在 Effect 内访问可变 state与在 Effect 内访问依赖dependency。读完后你将掌握捕获列表的正确写法、依赖的Sendable化改造方法并能理解这些规则在仓库源码层面的底层依据。一、为什么需要关注 Swift 并发安全1.1 从警告到错误Swift 6 的严格并发检查自 Swift 5.6 起编译器便能够对在并发上下文中使用非线程安全类型与函数的情况给出大量警告。这些警告在旧版本中可以被暂时忽略但在 Swift 6 中绝大部分如果不是全部警告会升级为编译错误。因此你不能再依赖编译能过就行的心态而必须掌握如何向编译器证明你的类型可以安全地用于并发场景。这一点在本仓库的工程配置中已有明确体现根目录 Package.swift 的swift-tools-version声明为6.1// swift-tools-version:6.1意味着库本身即在 Swift 6 严格并发模式下构建你基于它编写的 feature 代码同样要满足这一标准。1.2 TCA 中创建 Effect 的主要途径在 TCA 中创建Effect的首要方式是Effect.run(priority:operation:catch:fileID:filePath:line:column:)。从源码 Effect.swift 可以看到其完整签名public static func run( priority: TaskPriority? nil, name: String? nil, operation: escaping Sendable (_ send: SendAction) async throws - Void, catch handler: (Sendable (_ error: any Error, _ send: SendAction) async - Void)? nil, fileID: StaticString #fileID, filePath: StaticString #filePath, line: UInt #line, column: UInt #column ) - Self几个关键参数说明参数默认值作用prioritynil继承Task.currentPriority底层任务执行的优先级namenil可选的任务名称便于调试定位operation无异步闭包主体接收Send用于把 Action 发回系统catchnil错误处理闭包仅在抛出非取消类错误时被调用fileID/filePath/line/column编译期自动捕获用于在运行时警告中定位出错的 Effect 来源最关键的是operation闭包被标注为Sendable异步闭包这直接限制了可传入闭包的捕获类型闭包只能捕获以let绑定的Sendable变量。可变变量var和非Sendable类型都不允许被传入Sendable闭包。实现细节Effect.run内部通过withEscapedDependencies将当前依赖上下文转义进异步闭包再在do/catch中执行operation(send)。其中CancellationError会被静默吞掉直接返回其他错误在未提供catch处理器时会触发运行时警告详见 Effect.swift。1.3Send向系统回传 Action 的类型run闭包接收的send参数类型是SendAction它实现了callAsFunction因此可以直接以函数形式调用return .run { send in await send(.started) for await event in self.events { send(.event(event)) } await send(.finished) }Send被标注为MainActor且自身Sendable见 Effect.swift每次发送动作前都会先检查Task.isCancelled取消后发送将被忽略还支持携带动画或事务的发送await send(.started, animation: .spring()) await send(.finished, transaction: .init(animation: .default))二、在 Effect 中访问 state可变捕获问题与解决方案2.1 问题inoutstate 无法进入Sendable闭包Reducer 以可变inoutstate 变量执行而这类变量不能从Sendable闭包内访问。下面的写法在 Swift 6 下会直接编译报错Reducer struct Feature { ObservableState struct State { /* ... */ } enum Action { /* ... */ } var body: some ReducerState, Action { Reduce { state, action in switch action { case .buttonTapped: return .run { send in try await Task.sleep(for: .seconds(1)) await send(.delayed(state.count)) // Mutable capture of inout parameter state is // not allowed in concurrently-executing code } // ... } } } }原因在于state是以inout方式传入 reducer 闭包的闭包体内的run又是Sendable异步闭包。异步闭包可能在 reducer 返回之后才真正执行届时state的内存位置可能已被后续的 reducer 调用改写——这正是编译器要阻止的数据竞争。2.2 解法一显式捕获为不可变值将state显式捕获为不可变值供闭包作用域内使用return .run { [state] send in try await Task.sleep(for: .seconds(1)) await send(.delayed(state.count)) // ✅ }捕获列表中的[state]会生成一个let绑定的副本从而满足Sendable闭包对捕获变量的约束。注意这要求State本身是Sendable的ObservableState结构体在 TCA 中满足此条件。2.3 解法二只捕获最小必要部分推荐更精细的做法是只捕获 Effect 真正需要的字段通过绑定新变量名实现return .run { [count state.count] send in try await Task.sleep(for: .seconds(1)) await send(.delayed(count)) // ✅ }这种最小捕获不仅满足编译器要求还能避免闭包持有整个 state 副本减少不必要的值拷贝语义上也更清晰——Effect 只依赖它声明的数据。2.4 仓库中的实战印证上述两种写法在本仓库示例中有大量真实应用。以 03-Effects-Basics.swift 为例numberFactButtonTapped动作中获取数字趣味事实number factcase .numberFactButtonTapped: state.isNumberFactRequestInFlight true state.numberFact nil // Return an effect that fetches a number fact from the API and returns the // value back to the reducers numberFactResponse action. return .run { [count state.count] send in await send(.numberFactResponse(Result { try await self.factClient.fetch(count) })) }同样的模式还出现在03-Effects-Cancellation.swift[count state.count]03-Effects-Timers.swift[isTimerActive state.isTimerActive]05-HigherOrderReducers-ReusableFavoriting.swift一次捕获多个字段[id state.id, isFavorite state.isFavorite, favorite]05-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift[url state.url]值得注意上面的例子在 Effect 内还访问了self.factClient依赖这属于下面要讲的第二类场景。依赖之所以可以安全访问是因为依赖值本身已被库保证为Sendable。三、在 Effect 中访问依赖让依赖Sendable3.1 依赖必须Sendable的原因在 TCA 中开发者通过依赖dependency机制让 reducer 以确定性、受控的方式与外部世界交互。由于这些依赖会被异步并发上下文使用因此它们必须满足Sendable。依赖不Sendable时编译器会在你注册它的那一刻就给出提示。3.2 注册依赖时的编译期反馈当你扩展DependencyValues提供计算属性时extension DependencyValues { var factClient: FactClient { get { self[FactClient.self] } set { self[FactClient.self] newValue } } }如果FactClient不是Sendableget与set两行会立即出现警告⚠️ Type FactClient does not conform to the Sendable protocol这一警告的根源在于DependencyValues本身被设计为可跨并发上下文传递的值容器其内部通过下标self[FactClient.self]存取依赖底层依赖注册表要求键类型满足Sendable约束。3.3 修复让接口类型只持有Sendable数据修复方式通常是确保依赖的接口类型只持有Sendable数据尤其是基于闭包的端点应标注为Sendablestruct FactClient { var fetch: Sendable (Int) async throws - String }这样会把可传入的闭包限制为Sendable闭包进而让整个FactClient结构体自身变得Sendable。3.4 仓库中的完整依赖示例官方文档中的FactClient正是本仓库 CaseStudies 示例的核心依赖。完整实现在 FactClient.swift它展示了接口类型 live 实现 test 实现的标准三段式DependencyClient struct FactClient { var fetch: Sendable (Int) async throws - String } extension DependencyValues { var factClient: FactClient { get { self[FactClient.self] } set { self[FactClient.self] newValue } } } extension FactClient: DependencyKey { /// This is the live fact dependency that reaches into the outside world to fetch trivia. /// Typically this live implementation of the dependency would live in its own module so that the /// main feature doesnt need to compile it. static let liveValue Self( fetch: { number in try await Task.sleep(for: .seconds(1)) let (data, _) try await URLSession.shared .data(from: URL(string: http://number-trivia.com/\(number)/trivia)!) return String(decoding: data, as: UTF8.self) } ) /// This is the unimplemented fact dependency that is useful to plug into tests that you want /// to prove do not need the dependency. static let testValue Self() }可以提炼出三条可复用的工程实践闭包端点必须标注Sendablevar fetch: Sendable (Int) async throws - String是让依赖整体Sendable的关键。live 实现放在依赖自身的扩展里注释明确指出 live 实现通常应放在独立模块中这样主 feature 模块无需编译网络相关代码也避免 live 实现污染接口类型。用DependencyClient宏生成脚手架它为FactClient生成init等样板代码static let testValue Self()即未实现依赖用于测试中证明某功能不需要该依赖。四、深入底层Effect 的运行与取消机制4.1 Effect 在 Store 中的执行Effect的Operation枚举只有三种情况none、publisher和run见 Effect.swift。当 reducer 返回.run效果时Store 会在主 actor 上为它创建一个Task并执行见 Core.swiftcase .run(let name, let priority, let operation): withEscapedDependencies { continuation in let task Task(name: name, priority: priority) { MainActor [weak self] in let isCompleted LockIsolated(false) defer { isCompleted.setValue(true) } await operation( Send { effectAction in if isCompleted.value { reportIssue( An action was sent from a completed effect. ... ) } if let task continuation.yield({ self?.send(effectAction, origin: .effect) }) { tasks.withValue { $0.append(task) } } } ) self?.effectCancellables[uuid] nil } tasks.withValue { $0.append(task) } self.effectCancellables[uuid] AnyCancellable { Sendable in task.cancel() } }这段实现揭示了几条重要行为Effect 的run闭包在MainActor任务中执行而send的调度通过continuation.yield交还依赖上下文保证与 reducer 的状态访问一致如果 effect 完成后仍通过send发送动作会触发运行时警告An action was sent from a completed effect这是调试异步逻辑错位的得力工具每个 effect 都被登记进effectCancellables可以通过对应的AnyCancellable取消底层Task。4.2 取消与withTaskCancellationEffect.run之所以能与 TCA 的取消机制无缝协作是因为.cancellable(id:)与Effect.cancel(id:)在.run分支内部使用了withTaskCancellation(id:cancelInFlight:)见 Cancellation.swift。该工具函数以任务取消句柄实现带相同id的任务被Effect.cancel(id:)触发时即被取消而cancelInFlight: true可先取消同 id 的在途任务再启动新任务天然支持防抖debounceenum CancelID { case response } return .run { send in try await withTaskCancellation(id: CancelID.response, cancelInFlight: true) { try await self.clock.sleep(for: .seconds(0.3)) await send( .debouncedResponse(await Result { try await environment.request() }) ) } }4.3 错误的统一出口catch参数run的operation闭包允许抛错。除CancellationError外任何错误若未显式处理在模拟器/真机运行时会触发运行时警告在测试中则导致测试失败这正是 TCA 刻意设计的让错误无处可藏策略。正确的做法是通过catch尾随闭包显式处理return .run { send in try await self.api.fetch() } catch: { error, send in await send(.apiFailed(error)) }4.4 效果组合merge与concatenate掌握单个.run之后还需了解 Effect 的组合。merge让多个 effect 同时并发运行.run组合时底层使用withTaskGroupconcatenate则按顺序串行执行。二者均对.run分支做了原生并发实现见 Effect.swift保证了组合后的 effect 依然保持结构化并发的取消传播语义。五、实践清单向 Swift 6 平稳迁移综合以上分析编写安全并发 Effect 时可对照以下清单自查Effect 闭包只捕获let绑定的Sendable值——凡是涉及state的一律改用捕获列表[state]或[count state.count]不要直接引用inout参数只捕获 Effect 真正需要的字段降低拷贝成本并使数据依赖显式化所有依赖的接口类型必须Sendable——结构体中的闭包端点全部标注Sendable用DependencyValues扩展 DependencyKey提供 live/test 实现参考 FactClient.swift 的三段式结构显式处理非取消错误——用catch参数或do/catch块切勿依赖默认的运行时警告需要取消的 effect 使用Effect.cancellable(id:)与Effect.cancel(id:)配对防抖场景用cancelInFlight: true警惕完成后发送动作——若send被逃逸到非结构化上下文Store 会给出 An action was sent from a completed effect 警告确保run闭包在结束前完成所有send调用。按照这些规则改造后你的 reducer 代码将同时通过 Swift 5.6 的警告审查与 Swift 6 的严格并发检查为语言升级扫清障碍。更多相关背景可继续阅读本仓库的 DependencyManagement.md依赖注册与覆盖机制与 TestingTCA.md在测试中验证异步 effect 行为。赞分享前端移动开发【免费下载链接】swift-composable-architectureA library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind.项目地址https://gitcode.com/GitHub_Trending/sw/swift-composable-architecture点击查看免费下载相关推荐Salvattore性能优化减少重排的3个关键技巧Salvattore性能优化减少重排的3个关键技巧 Salvattore作为一款CSS驱动的瀑布流布局库以其轻量级和灵活配置深受开发者喜爱。然而在处理大量动前端Swift Composable Architecture并发模型Async/Await和Actor集成Swift Composable Architecture并发模型Async/Await和Actor集成 痛点现代Swift并发编程的挑战 在Swift 5前端移动开发Swift Composable Architecture Server-Side Swift全栈应用开发Swift Composable Architecture Server Side Swift全栈应用开发 前言为什么需要Server Side Swift前端移动开发上一篇Machine Learning Fundamentals下一篇10分钟上手Light Bootstrap Dashboard React快速搭建响应式后台系统创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表