在 Combine 中,filter 和 compactMap 虽然看起来都能起到“减少数据量”的作用,但它们的内部逻辑、类型推导以及对 CPU 的占用方式存在细微但关键的差异。
1. 内部机制的本质区别
filter:值的筛选器
filter 的逻辑极其简单。它像是一个开关闸门。
- 机制:它接收一个返回
Bool的闭包。如果为true,值原封不动透传;如果为false,值被丢弃。 - 类型系统:不改变 Output 类型。如果上游是
Int,下游依然是Int。 - 内部实现:仅进行一次逻辑判断,不涉及内存分配或类型转换。
compactMap:值的重塑与解包器
compactMap 实际上是 map + filter(not nil) + unwrap 的组合体。
-
机制:它接收一个返回
Optional的闭包。- 执行转换逻辑。
- 检查结果是否为
nil。 - 如果不是
nil,将其**解包(Unwrap)**并发送。
-
类型系统:改变 Output 类型。它能将
Publisher<String?, Never>转换为Publisher<String, Never>。 -
内部实现:涉及一次函数调用(转换)、一次判空、一次解包。
2. 性能差异分析
在大多数 UI 开发场景下,两者的差异可以忽略不计。但在高频数据流(如每秒万级的传感器数据或大批量数据处理)中,差异开始显现:
| 维度 | filter | compactMap |
|---|---|---|
| 计算开销 | 极低。仅需一个指令周期的布尔判断。 | 中等。需要执行闭包转换并产生一个临时 Optional 对象。 |
| 内存压力 | 无。不产生新对象。 | 轻微。在栈上创建和销毁 Optional 容器。 |
| 指令路径 | 短。判断 -> 发送。 | 较长。转换 -> 包装 -> 判空 -> 解包 -> 发送。 |
性能结论: 如果你仅仅是为了过滤掉某些已知的值(且不需要改变类型),filter 的效率总是高于 compactMap。
3. 典型应用场景对比
场景 A:纯粹的条件拦截(选 filter)
Swift
// 仅仅检查数值大小,不涉及类型变化
numbersPublisher
.filter { $0 > 10 }
场景 B:类型转换与安全过滤(选 compactMap)
Swift
// 尝试将字符串转数字,失败时(nil)自动过滤
stringPublisher
.compactMap { Int($0) }
4. 一个隐蔽的“性能陷阱”
很多开发者为了代码简洁,会写出这样的代码:
Swift
// ❌ 性能较差的写法
publisher
.compactMap { $0.isValid ? $0 : nil }
// ✅ 性能更好的写法
publisher
.filter { $0.isValid }
在第一种写法中,你强行把一个本来就存在的对象包装进 Optional,然后再让 Combine 把它解包出来。这不仅增加了 CPU 负担,还让代码的意图变得模糊。
5. 组合拳:防御式性能优化
如果你面临极端性能需求(例如处理数百万个元素的 Sequence Publisher),请遵循以下顺序:
- 先 filter 再 map:先剔除掉 90% 的无效数据,再对剩下的 10% 执行复杂的
map转换。 - 避免在 compactMap 里写重逻辑:如果转换过程非常耗时且失败率高,考虑先用一个轻量级的
filter把明显不合格的数据拦住。
总结
filter是“阅后即焚”的保安,只看通行证,不换衣服。compactMap是“海关”,既要检查(过滤 nil),又要帮你换外币(类型转换)。