如何简化/泛化包含不同子任务返回类型的任务组逻辑?
如何简化/泛化包含不同子任务返回类型的任务组逻辑?
这个问题我太有共鸣了——每次写这种多类型返回的TaskGroup,那些枚举、switch和临时变量真的是烦死人!我之前也踩过同样的坑,后来摸索出几个能大幅减少样板代码的方案,分享给你:
方案1:针对固定数量任务的泛型工具函数
这是最直接的方式,针对你常用的任务数量(比如2个、3个不同类型的子任务),把所有重复的枚举、switch逻辑都封装在泛型工具函数内部,你只需要传入任务闭包就行。
比如针对返回(Int, String)的场景,我们可以这么写:
// 类型擦除的容器,用来统一TaskGroup的元素类型 private protocol AnyTaskResultMarker {} private enum AnyTaskResult<Value>: AnyTaskResultMarker { case value(Value) } // 泛型工具函数:支持两个不同类型的任务 func withTwoMixedTasks<A, B>( task1: @escaping () async throws -> A, task2: @escaping () async throws -> B ) async throws -> (A, B) { try await withThrowingTaskGroup(of: AnyTaskResultMarker.self) { group in // 添加两个任务,用AnyTaskResult包装结果 group.addTask { AnyTaskResult.value(try await task1()) } group.addTask { AnyTaskResult.value(try await task2()) } // 初始化结果变量 var resultA: A? var resultB: B? // 遍历收集结果,自动匹配类型 for try await result in group { switch result { case let .value(a) as AnyTaskResult<A>: resultA = a case let .value(b) as AnyTaskResult<B>: resultB = b default: fatalError("收到了意料之外的任务结果类型") } } // 强制解包是安全的:每个任务都会返回对应类型的结果 return (resultA!, resultB!) } }
使用的时候就超级简洁:
let (intValue, stringValue) = try await withTwoMixedTasks( task1: getInt, task2: getString )
如果需要支持3个不同类型的任务,只需要复制这个函数改成withThreeMixedTasks,增加对应的泛型参数和任务闭包就行——虽然还是有点重复,但比起每次手动写枚举和switch,已经省了太多事。
方案2:泛化的结果收集器(支持任意数量任务)
如果你的场景里子任务数量不固定,或者想更灵活,可以用一个类型安全的结果收集器,结合TaskGroup自动处理结果赋值:
// 持有结果引用的容器,用来接收子任务返回值 final class ResultSlot<Value> { private(set) var value: Value? func set(_ value: Value) { self.value = value } } // 类型擦除的任务结果容器 private protocol AnyTaskResult { func fillResultSlots(_ slots: [AnyObject]) } private enum ConcreteTaskResult<Value>: AnyTaskResult { case result(Value) func fillResultSlots(_ slots: [AnyObject]) { guard let slot = slots.first(where: { $0 is ResultSlot<Value> }) as? ResultSlot<Value>, case let .result(value) = self else { return } slot.set(value) } } // 泛化的任务组工具函数 func withMixedTaskGroup<Result>( @_implicitSelfCapture build: (inout MixedTaskGroupCollector) async throws -> Void, then assemble: () throws -> Result ) async throws -> Result { var collector = MixedTaskGroupCollector() try await build(&collector) try await withThrowingTaskGroup(of: AnyTaskResult.self) { group in // 添加所有收集器里的任务 for task in collector.tasks { group.addTask { try await task() } } // 遍历结果,自动填充到对应的ResultSlot for try await result in group { result.fillResultSlots(collector.slots) } } // 组装最终的结果元组/结构体 return try assemble() } // 任务收集器的辅助类 class MixedTaskGroupCollector { fileprivate var tasks: [() async throws -> AnyTaskResult] = [] fileprivate var slots: [AnyObject] = [] // 添加子任务,返回对应的结果槽位 func addTask<Value>(_ task: @escaping () async throws -> Value) -> ResultSlot<Value> { let slot = ResultSlot<Value>() slots.append(slot) tasks.append { ConcreteTaskResult.result(try await task()) } return slot } }
使用方式是这样的:
let (intValue, stringValue) = try await withMixedTaskGroup { collector in // 为每个任务创建结果槽位 let intSlot = collector.addTask(getInt) let stringSlot = collector.addTask(getString) } then: { // 从槽位里取出结果并组装 guard let int = intSlot.value, let string = stringSlot.value else { fatalError("任务未返回结果") } return (int, string) }
这个方案支持任意数量的不同类型任务,不用每次写新的泛型函数——代价是内部实现稍微复杂一点,但用起来比手动写TaskGroup的样板代码舒服多了。
再聊聊你提到的async let
你说的没错,async let确实简洁,但它没办法实现你需要的“短路”效果:当你用try await (a, b)时,哪怕其中一个任务立刻抛出错误,Swift还是会等待所有async let任务完成后才会抛出错误,父任务没办法提前终止。而TaskGroup的核心优势就在这里:只要有任何一个子任务抛出,TaskGroup会立刻取消所有未完成的子任务,父任务可以马上处理错误,这在对性能敏感或者需要快速失败的场景里非常重要。
内容来源于stack exchange
相关产品推荐
相关产品推荐

