Combine 高级操作符:掌控数据流的节奏与方向

如果说基础操作符是对单个值的简单加工,那么高级操作符就是掌管整个数据流生命周期、资源分配和错误恢复的"总调度"。它们能帮你处理高频率的数据、在多个订阅者之间共享昂贵操作、按时间窗口掐断冗余请求,甚至定制自己的操作符。本章将分类拆解这些强大工具,并附上完整可运行的代码示例。

1. 操作符分类概览

Combine 内置了数十个操作符,根据功能大致可分为以下六类:

mindmap root((高级操作符)) 转换 map / tryMap / compactMap flatMap / switchToLatest reduce / scan 过滤 filter / removeDuplicates first / last dropFirst / drop(while:) prefix / prefix(while:) ignoreOutput 组合 combineLatest / zip / merge append / prepend 时间 debounce / throttle delay / timeout measureInterval 流控制与共享 buffer / collect share / multicast eraseToAnyPublisher 错误处理 catch / tryCatch replaceError / retry assertNoFailure

下面我们将逐一深入每个类别。

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. 最佳实践

流控制顺序 :先过滤后转换,debounceremoveDuplicates 放在 flatMap 前面,减少无效网络请求。

类型擦除 :模块边界(如 Service 方法)返回 AnyPublisher,保持 API 简洁稳定。

错误处理内聚 :在 flatMap 返回的内部 Publisher 中使用 catch,防止外层订阅终止。

资源管理 :对昂贵请求使用 share()multicast,配合 Set<AnyCancellable> 统一管理生命周期。

避免循环引用 :闭包中坚持使用 [weak self]

性能监控 :利用 measureIntervalhandleEvents 观察关键节点耗时。

11. 总结

高级操作符是 Combine 完成复杂异步任务的利器。从状态变更的精确控制(debounce/throttle),到多订阅者的资源共享(share/multicast),再到流自身的容错和恢复(catch/retry),它们将响应式编程的表达力推向了新的高度。建议在 Playground 中逐一实验,建立起对数据流时机的直觉,最终才能在实际项目中游刃有余。

相关推荐
黑化旺仔6 小时前
【OC】KVO
macos·ios·objective-c·cocoa
HouWan8 小时前
SwiftUI 小技巧:如何读取 SwiftUI Font 的详细信息
ios·swiftui·apple
李游Leo8 小时前
HarmonyOS 7 实战开发 05:把页面做到可上线状态
ios·harmonyos
终端安全笔记14 小时前
描述文件、企业证书、MDM 不是三个名字,是三层
android·ios·智能手机
传奇开心果编程14 小时前
【SwiftUI入门练中学】第1课 从零开始
学习·ui·ios·swiftui·swift
丿Wayne20 小时前
iOS Token 刷新实战(Actor 避免并发重复刷新)
ios·swift·token·actor·swift并发
终端安全笔记2 天前
iOS 27 给了租赁一个新工具,但它只认受监督的设备
android·网络·安全·ios
大熊猫侯佩2 天前
独立 App 首发完成 iPhone Duo 展开态适配
ios·swiftui·swift
光影少年2 天前
setImmediate 和 setTimeout(0) 的区别
android·前端·react.js·ios·前端框架
2501_916008892 天前
SSE 和 gRPC 流式接口如何抓包,调试 SSE 与 gRPC 流式接口
网络协议·计算机网络·网络安全·ios·adb·https·udp