async/await结合Combine防抖时,如何让泛型T自动推断?
项目需实现多类型搜索,通用搜索方法采用泛型T,通过多个简易方法按需指定类型调用。希望实现搜索延迟启动(即Combine中的debounce防抖效果),并尽可能将防抖逻辑隐藏在底层。
项目主要使用async/await API,仅在必要时使用Combine。由于未找到async/await内置的防抖方案,计划采用Combine的debounce实现(若存在无需Combine的更简洁方案,欢迎告知)。
但在async/await与Combine间转换时,使用withCheckedContinuation会丢失泛型T,导致类型推断报错,相关代码如下:
// 指定Event类型的简易搜索方法 func searchEvents(query: String) async -> [Event] { await search(query: query) } // 指定Artist类型的简易搜索方法 func searchArtists(query: String) async -> [Artist] { await search(query: query) } // 简易方法与通用搜索之间的防抖层 private func search<T>(query: String) async -> [T] where T: Codable, T: Identifiable { await withCheckedContinuation { continuation in Debouncer().debounce(query) { query in Task { // 错误提示:无法推断泛型参数'T' let a = await self.performSearch(query: query) // <- 报错位置 continuation.resume(returning: a) } } } } // 通用搜索方法,具体实现无关紧要 private func performSearch<T>(query: String) async -> [T] where T: Codable, T: Identifiable { ... } // 实现简易防抖的类 class Debouncer { static var shared = Debouncer() private var debounceSubscription: AnyCancellable? func debounce(_ string: String, block: (String) -> ()) { debounceSubscription?.cancel() debounceSubscription = Just(string) .debounce(for: .milliseconds(200), scheduler: DispatchQueue.global(qos: .background)) .sink { string in block(string) } } }
请问有没有优雅的方式实现类型自动推断,无需传递type: T.Type参数?
编辑:上述方案最终无法正常工作,我只能直接在TextField上通过Combine处理:
.onChange(query) { query in queryPublisher.send(query) } .onReceive(queryPublisher.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)) { query in Task { options = await fetchOptions(query) } }
我原本想避免这种写法,因为应用中有大量文本框需要搜索功能,但创建基类比将防抖逻辑隐藏在底层更简单。感谢大家的建议,我现在已掌握泛型的修复方法!
1. 修复泛型推断问题
闭包上下文丢失了外层的泛型T信息,导致performSearch无法自动推断类型。只需显式指定泛型参数即可解决,无需额外传递类型参数:
private func search<T>(query: String) async -> [T] where T: Codable, T: Identifiable { await withCheckedContinuation { continuation in Debouncer().debounce(query) { [weak self] query in guard let self else { return } Task { // 显式指定泛型T,让performSearch捕获外层类型 let result = await self.performSearch<T>(query: query) continuation.resume(returning: result) } } } }
2. 纯async/await的防抖方案(无需Combine)
如果不想依赖Combine,可以基于Task实现轻量的异步防抖工具:
actor AsyncDebouncer { private var pendingTask: Task<Void, Never>? func debounce(interval: Duration, action: @escaping () async -> Void) { pendingTask?.cancel() pendingTask = Task { try? await Task.sleep(for: interval) guard !Task.isCancelled else { return } await action() } } }
集成到通用搜索逻辑中:
private let debouncer = AsyncDebouncer() private func search<T>(query: String) async -> [T] where T: Codable, T: Identifiable { await withCheckedContinuation { continuation in Task { await debouncer.debounce(interval: .milliseconds(200)) { let result = await self.performSearch<T>(query: query) continuation.resume(returning: result) } } } }
该方案完全基于async/await生态,无需引入Combine,更贴合项目技术栈。
3. 批量文本框的复用优化
针对大量需要搜索功能的文本框,除了创建基类,还可以封装通用ViewModifier来复用逻辑:
struct SearchTextFieldModifier<T: Codable & Identifiable>: ViewModifier { @Binding var query: String @Binding var results: [T] let searchAction: (String) async -> [T] let debounceInterval: Duration = .milliseconds(500) private let debouncer = AsyncDebouncer() func body(content: Content) -> some View { content .onChange(of: query) { newQuery in Task { await debouncer.debounce(interval: debounceInterval) { results = await searchAction(newQuery) } } } } } // 扩展View简化调用 extension View { func searchable<T: Codable & Identifiable>(query: Binding<String>, results: Binding<[T]>, searchAction: @escaping (String) async -> [T]) -> some View { self.modifier(SearchTextFieldModifier(query: query, results: results, searchAction: searchAction)) } }
使用时只需给TextField添加该修饰符:
TextField("搜索事件", text: $eventQuery) .searchable(query: $eventQuery, results: $events) { query in await searchEvents(query: query) }
这样所有搜索文本框都能复用防抖和搜索逻辑,避免重复代码,也降低了维护成本。
内容的提问来源于stack exchange,提问作者f3dm76

