如果说基础操作符是对单个值的简单加工,那么高级操作符就是掌管整个数据流生命周期、资源分配和错误恢复的"总调度"。它们能帮你处理高频率的数据、在多个订阅者之间共享昂贵操作、按时间窗口掐断冗余请求,甚至定制自己的操作符。本章将分类拆解这些强大工具,并附上完整可运行的代码示例。
1. 操作符分类概览
Combine 内置了数十个操作符,根据功能大致可分为以下六类:
下面我们将逐一深入每个类别。
2. 转换操作符
2.1 map / tryMap
map 是最简单的转换操作符;tryMap 则允许在转换过程中抛出错误。
swift
let numbers = [1, 2, 3].publisher
numbers
.map { $0 * 2 }
.sink { print($0) } // 2, 4, 6
let strings = ["1", "2", "X"].publisher
strings
.tryMap { str -> Int in
guard let n = Int(str) else { throw URLError(.badURL) }
return n
}
.sink(receiveCompletion: { print("完成: \($0)") },
receiveValue: { print($0) })
2.2 compactMap
自动过滤 nil,常用于字符串转数字等场景。
swift
let values = ["1", "2", "three", "4"].publisher
values
.compactMap { Int($0) }
.sink { print($0) } // 1, 2, 4
2.3 flatMap 与 maxPublishers
flatMap 将上游每个值映射为一个新的 Publisher,并展平其输出。通过 maxPublishers 可限制并发数。
swift
let searchText = PassthroughSubject<String, Never>()
searchText
.flatMap(maxPublishers: .max(2)) { query -> AnyPublisher<String, Never> in
// 模拟网络请求
Just("\(query) 的结果")
.delay(for: .seconds(1), scheduler: DispatchQueue.main)
.eraseToAnyPublisher()
}
.sink { print($0) }
searchText.send("A")
searchText.send("B")
searchText.send("C") // 等前两个之一完成后才会处理
2.4 switchToLatest
当上游发出新的 Publisher 时,自动取消前一个 Publisher 的订阅,永远只接收最新值。搜索建议的最佳选择。
swift
let subject = PassthroughSubject<String, Never>()
subject
.map { query in
Just("\(query) 的结果")
.delay(for: .seconds(1), scheduler: DispatchQueue.main)
}
.switchToLatest()
.sink { print($0) }
subject.send("S") // 会被随后的发送取消
subject.send("Swift") // 输出 "Swift 的结果"
2.5 reduce 与 scan
reduce在所有上游值发送完成后,输出一个聚合值。scan输出每一次累积的中间值,适合进度、累计等需求。
swift
let numbers = [1, 2, 3, 4, 5].publisher
numbers
.reduce(0, +)
.sink { print("总和: \($0)") } // 15
numbers
.scan(0, +)
.sink { print("累积: \($0)") } // 1, 3, 6, 10, 15
3. 过滤操作符
3.1 filter
所有不满足条件的值都会被丢弃。
swift
[1, 2, 3, 4].publisher
.filter { $0 % 2 == 0 }
.sink { print($0) } // 2, 4
3.2 removeDuplicates
连续重复的值只会保留第一个,直到下次变化。
swift
[1, 1, 2, 2, 3, 1].publisher
.removeDuplicates()
.sink { print($0) } // 1, 2, 3, 1
3.3 first / last
first(where:)找到满足条件的第一个值后立即完成。last(where:)等待上游完成后才发出最后一个满足条件的值。
swift
[2, 4, 6, 8].publisher
.first { $0 > 5 }
.sink { print($0) } // 6
[1, 3, 5, 7].publisher
.last { $0 < 5 }
.sink { print($0) } // 3
3.4 dropFirst / drop(while:)
dropFirst(_:)忽略前 n 个值。drop(while:)一直丢弃直到条件不再满足。
swift
[1, 2, 3, 4, 5].publisher
.dropFirst(2)
.sink { print($0) } // 3, 4, 5
[1, 2, 3, 4, 5].publisher
.drop { $0 < 3 }
.sink { print($0) } // 3, 4, 5
3.5 prefix / prefix(while:)
prefix(_:)只取前 n 个值后完成。prefix(while:)一直取值直到条件不再满足。
swift
[1, 2, 3, 4, 5].publisher
.prefix(3)
.sink { print($0) } // 1, 2, 3
[1, 2, 3, 4, 5].publisher
.prefix { $0 < 4 }
.sink { print($0) } // 1, 2, 3
3.6 ignoreOutput
完全忽略所有值,只关心完成事件。
swift
[1, 2, 3].publisher
.ignoreOutput()
.sink(receiveCompletion: { print("完成") },
receiveValue: { _ in print("不会输出") })
4. 组合操作符
4.1 combineLatest
任一 Publisher 发出新值,就与其他 Publisher 的最新值组成元组发出。
swift
let user = PassthroughSubject<String, Never>()
let pass = PassthroughSubject<String, Never>()
user.combineLatest(pass)
.map { u, p in !u.isEmpty && p.count >= 6 }
.sink { print("表单有效: \($0)") }
user.send("admin")
pass.send("123") // false
pass.send("123456") // true
4.2 zip
严格按顺序配对,双方都有新值才输出。元素数量不对等时,多出的会被丢弃。
swift
let nums = [1, 2, 3].publisher
let letters = ["A", "B", "C", "D"].publisher
nums.zip(letters)
.sink { num, letter in
print("\(num)-\(letter)")
}
// 1-A, 2-B, 3-C
4.3 merge
将多个 Publisher 的值交织合并到一个序列中,顺序取决于发送时机。
swift
let one = PassthroughSubject<Int, Never>()
let two = PassthroughSubject<Int, Never>()
one.merge(with: two)
.sink { print($0) }
one.send(1)
two.send(2)
one.send(3)
two.send(4) // 输出 1,2,3,4
4.4 append / prepend
在序列末尾或开头插入值。
swift
[2, 3].publisher
.prepend(1)
.append(4, 5)
.sink { print($0) } // 1,2,3,4,5
5. 时间操作符
5.1 debounce
静默一段时间后,若没有新值则发送最后一个值。搜索输入的标准配置。
swift
let input = PassthroughSubject<String, Never>()
input
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
.sink { print("搜索: \($0)") }
input.send("S")
input.send("Sw")
input.send("Swi")
input.send("Swift") // 300ms 后输出 "Swift"
5.2 throttle
在固定时间窗内,只发送第一个或最后一个值。latest: true 发送最后一个,false 发送第一个。
swift
let fastPublisher = PassthroughSubject<Int, Never>()
fastPublisher
.throttle(for: .seconds(1), scheduler: DispatchQueue.main, latest: true)
.sink { print("节流: \($0)") }
// 在一秒内发送多个值,只有最后一个会被打印
debounce 与 throttle 的选择:
- debounce 适合搜索框,等待用户停止输入。
- throttle 适合滚动监听、按钮防连点等,保证固定频率的更新。
5.3 delay / timeout
delay延迟所有值和完成的发送。timeout在指定时间内未收到值则触发错误完成。
swift
Just("延迟消息")
.delay(for: .seconds(2), scheduler: DispatchQueue.main)
.sink { print($0) }
let subject = PassthroughSubject<String, Never>()
subject
.timeout(.seconds(5), scheduler: DispatchQueue.main)
.sink(receiveCompletion: { print("超时: \($0)") },
receiveValue: { print($0) })
5.4 measureInterval
测量相邻两个值之间的时间间隔。
swift
Timer.publish(every: 1, on: .main, in: .common).autoconnect()
.measureInterval(using: DispatchQueue.main)
.sink { print("间隔: \($0)") }
6. 流控制与共享操作符
6.1 buffer
设置缓冲区大小,当缓冲区满时根据策略丢弃最旧或最新的值。
swift
let subject = PassthroughSubject<Int, Never>()
subject
.buffer(size: 3, prefetch: .byRequest, whenFull: .dropOldest)
.sink { print($0) }
.store(in: &cancellables)
subject.send(1)
subject.send(2)
subject.send(3)
subject.send(4) // 缓冲区满,丢弃1,输出 2,3,4
6.2 collect
将多个值收集为一个数组,可按指定数量分批收集。
swift
[1,2,3,4,5].publisher
.collect(2)
.sink { print($0) } // [1,2], [3,4], [5]
6.3 share
让多个订阅者共享同一个订阅,避免重复执行昂贵操作。通常与 makeConnectable() 或 autoconnect() 搭配。
swift
let shared = someExpensivePublisher
.share()
// 两个订阅者使用同一底层网络请求
shared.sink { ... }.store(in: &cancellables)
shared.sink { ... }.store(in: &cancellables)
6.4 multicast
比 share 更灵活,需要传入一个 Subject,通过手动 connect() 启动。
swift
let subject = PassthroughSubject<String, Never>()
let pub = ["A","B"].publisher.multicast(subject: subject)
pub.sink { print("sub1: \($0)") }.store(in: &cancellables)
pub.sink { print("sub2: \($0)") }.store(in: &cancellables)
pub.connect().store(in: &cancellables)
// sub1 和 sub2 同时收到 A, B
6.5 eraseToAnyPublisher
将复杂的 Publisher 类型擦除为 AnyPublisher,隐藏实现细节,便于 API 返回。
swift
func search(_ query: String) -> AnyPublisher<[String], Error> {
URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.decode(...)
.eraseToAnyPublisher()
}
7. 错误处理操作符
7.1 catch / tryCatch
catch 捕获错误后返回一个替代 Publisher,保证流不会因错误中断。 tryCatch 允许在捕获时再抛出错误。
swift
let failingPub = Fail<String, Error>(error: NSError(domain: "test", code: 0))
failingPub
.catch { _ in Just("恢复值") }
.sink { print($0) } // "恢复值"
7.2 replaceError
将错误替换为一个固定默认值。
swift
failingPub
.replaceError(with: "默认")
.sink { print($0) } // "默认"
7.3 retry
在发生错误时自动重新订阅,可指定重试次数。
swift
var attempt = 0
let flakyPub = Future<Int, Error> { promise in
attempt += 1
attempt < 3 ? promise(.failure(...)) : promise(.success(42))
}
flakyPub.retry(3)
.sink(receiveCompletion: { print($0) },
receiveValue: { print($0) })
7.4 assertNoFailure
调试工具,遇到错误会导致 Debug 崩溃,不可用于 Release。
swift
Just(1)
.assertNoFailure()
.sink { print($0) }
8. 自定义操作符
通过 extension Publisher 可以封装经常出现的重复逻辑。
swift
extension Publisher {
/// 忽略值,只关心完成
func ignoreValues() -> Publishers.Map<Self, Void> {
map { _ in }
}
/// 自动解包 Optional,过滤 nil
func unwrap<T>() -> Publishers.CompactMap<Self, T> where Output == Optional<T> {
compactMap { $0 }
}
/// 便捷日志打印
func log(_ prefix: String = "") -> Publishers.HandleEvents<Self> {
handleEvents(
receiveSubscription: { print("\(prefix) 订阅") },
receiveOutput: { print("\(prefix) 值: \($0)") },
receiveCompletion: { print("\(prefix) 完成: \($0)") },
receiveCancel: { print("\(prefix) 取消") }
)
}
}
// 使用
[1, nil, 3].publisher
.unwrap()
.log("测试")
.sink { print($0) }
9. 完整操作符链实战:智能搜索
下面是一个融合了防抖、去重、网络请求、错误处理和加载状态的完整 ViewModel。
swift
import Combine
import Foundation
class SmartSearchViewModel: ObservableObject {
@Published var query = ""
@Published var results: [String] = []
@Published var isLoading = false
@Published var errorMessage: String?
private var bag = Set<AnyCancellable>()
init() {
$query
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main) // 防抖
.removeDuplicates() // 去重
.filter { !$0.isEmpty }
.handleEvents(receiveOutput: { [weak self] _ in
self?.isLoading = true
self?.errorMessage = nil
})
.flatMap { query -> AnyPublisher<[String], Error> in
// 模拟网络请求,实际替换为 URLSession
let url = URL(string: "https://api.example.com/search?q=\(query)")!
return URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: [String].self, decoder: JSONDecoder())
.eraseToAnyPublisher()
}
.retry(1) // 失败自动重试一次
.catch { error -> AnyPublisher<[String], Never> in
Just([]).eraseToAnyPublisher() // 降级为空数组
}
.receive(on: DispatchQueue.main)
.handleEvents(receiveOutput: { [weak self] _ in
self?.isLoading = false
})
.assign(to: \.results, on: self)
.store(in: &bag)
}
}
10. 最佳实践
流控制顺序 :先过滤后转换,debounce 和 removeDuplicates 放在 flatMap 前面,减少无效网络请求。
类型擦除 :模块边界(如 Service 方法)返回 AnyPublisher,保持 API 简洁稳定。
错误处理内聚 :在 flatMap 返回的内部 Publisher 中使用 catch,防止外层订阅终止。
资源管理 :对昂贵请求使用 share() 或 multicast,配合 Set<AnyCancellable> 统一管理生命周期。
避免循环引用 :闭包中坚持使用 [weak self]。
性能监控 :利用 measureInterval 或 handleEvents 观察关键节点耗时。
11. 总结
高级操作符是 Combine 完成复杂异步任务的利器。从状态变更的精确控制(debounce/throttle),到多订阅者的资源共享(share/multicast),再到流自身的容错和恢复(catch/retry),它们将响应式编程的表达力推向了新的高度。建议在 Playground 中逐一实验,建立起对数据流时机的直觉,最终才能在实际项目中游刃有余。