13-14.【Combine】filter、compactMap 的内部机制有什么性能差异?

25 阅读3分钟

在 Combine 中,filtercompactMap 虽然看起来都能起到“减少数据量”的作用,但它们的内部逻辑、类型推导以及对 CPU 的占用方式存在细微但关键的差异。


1. 内部机制的本质区别

filter:值的筛选器

filter 的逻辑极其简单。它像是一个开关闸门

  • 机制:它接收一个返回 Bool 的闭包。如果为 true,值原封不动透传;如果为 false,值被丢弃。
  • 类型系统不改变 Output 类型。如果上游是 Int,下游依然是 Int
  • 内部实现:仅进行一次逻辑判断,不涉及内存分配或类型转换。

compactMap:值的重塑与解包器

compactMap 实际上是 map + filter(not nil) + unwrap 的组合体。

  • 机制:它接收一个返回 Optional 的闭包。

    1. 执行转换逻辑。
    2. 检查结果是否为 nil
    3. 如果不是 nil,将其**解包(Unwrap)**并发送。
  • 类型系统改变 Output 类型。它能将 Publisher<String?, Never> 转换为 Publisher<String, Never>

  • 内部实现:涉及一次函数调用(转换)、一次判空、一次解包。


2. 性能差异分析

在大多数 UI 开发场景下,两者的差异可以忽略不计。但在高频数据流(如每秒万级的传感器数据或大批量数据处理)中,差异开始显现:

维度filtercompactMap
计算开销极低。仅需一个指令周期的布尔判断。中等。需要执行闭包转换并产生一个临时 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),请遵循以下顺序:

  1. 先 filter 再 map:先剔除掉 90% 的无效数据,再对剩下的 10% 执行复杂的 map 转换。
  2. 避免在 compactMap 里写重逻辑:如果转换过程非常耗时且失败率高,考虑先用一个轻量级的 filter 把明显不合格的数据拦住。

总结

  • filter 是“阅后即焚”的保安,只看通行证,不换衣服。
  • compactMap 是“海关”,既要检查(过滤 nil),又要帮你换外币(类型转换)。