6-20.【架构设计】Combine / Rx 中,map / flatMap / switchLatest 的本质区别是什么?在架构层面你如何选择?

17 阅读4分钟

一、本质区别

假设有一个 Publisher(Combine / Rx):

sourcePublisher: A

你想根据它生成 B 类型的数据流,可以用 map / flatMap / switchLatest,但它们的处理方式本质不同


1️⃣ map

sourcePublisher.map { a in f(a) }
  • 本质:一对一转换

  • 类型A -> B

  • 行为

    • 每个输入事件 a → 输出 f(a)
    • 输出流长度 = 输入流长度
  • 异步性

    • 输出值同步产生(除非 f 返回 Publisher,再用 flatMap 包裹)
  • 特点:简单、轻量、不会改变事件流数量

💡 工程直觉:用来做单次同步转换,比如 Int -> StringModel -> ViewModel


2️⃣ flatMap

sourcePublisher.flatMap { a in asyncPublisher(a) }
  • 本质:把一个事件映射成一个新的 Publisher,并把这些内部 Publisher 的事件“合并”到同一个输出流

  • 类型A -> Publisher<B> → 输出 B

  • 行为

    • 所有内部 Publisher 并行订阅
    • 输出事件顺序可能交错
  • 异步性

    • 支持异步任务、网络请求等
  • 特点

    • 每个输入事件触发一个新的 Publisher
    • 输出流混合所有内部 Publisher 的事件
  • 🔥 风险:高频输入 → 多个并行网络请求 → 可能资源浪费 / 并发混乱

💡 工程直觉:输入事件触发独立异步任务,需要并行处理并保留所有结果


3️⃣ switchLatest (Combine) / flatMapLatest (Rx)

sourcePublisher
  .map { a in asyncPublisher(a) }
  .switchToLatest()
  • 本质:内部 Publisher 可切换,只保留最新的内部 Publisher 的输出

  • 行为

    • 新输入事件 → 取消上一个内部 Publisher
    • 输出只来自最新 Publisher
  • 异步性

    • 适合“只关心最新任务结果”场景
  • 特点

    • 避免旧任务干扰 UI
    • 控制并发资源占用

💡 工程直觉:输入是高频事件(如搜索、滑动、输入) → 只关心最新结果


二、行为总结表

Operator输入 → 输出并发保留旧事件典型场景
map1:1 转换N/A数据映射 / Model → ViewModel
flatMap1 → Publisher → 输出合并并行并行网络请求 / 多任务汇总
switchLatest1 → Publisher → 输出最新串行(旧取消)否(取消旧任务)高频输入 → 只保留最新结果

三、架构层面的选择原则

1️⃣ map → 同步 / 派生状态

  • 用于 State → DerivedStateModel → ViewModel
  • 不改变事件流结构
  • 性能轻,预测性好

2️⃣ flatMap → 并行异步操作

  • 用于 State 触发多任务

  • 保留所有结果 → Reducer 或 Adapter 处理

  • 注意:

    • 并发数量控制(避免 flood / race)
    • 输出混合 → Reducer 必须能处理乱序数据

3️⃣ switchLatest → 高频事件 + 最新任务

  • 用于 UI 高频事件(搜索框、滑动、节流动画)
  • 自动取消旧任务 → 减少资源占用
  • 可与 State / Reducer 结合 → 保持 UI 一致性

四、架构落地直觉

输入事件 → 映射转换 → 异步任务 → Reducer / Store
  • map → State 派生 / ViewModel 映射
  • flatMap → 并行 Effect / 网络 / DB
  • switchLatest → 高频输入 → 异步请求 → 保持最新 UI

核心原则:选择 Operator = 根据事件频率 + 是否保留历史 + 是否需要并行处理

英文版

[Architecture Design] Combine / Rx: What are the fundamental differences between map, flatMap, and switchLatest? How do you choose at the architectural level?

I. Fundamental Differences

Assuming you have a source Publisher (Combine / Rx):

Swift

sourcePublisher: A

To generate a stream of type B, you can use map, flatMap, or switchLatest, but their underlying execution models differ fundamentally.


1️⃣ map

Swift

sourcePublisher.map { a in f(a) }
  • Essence: One-to-one synchronous transformation.

  • Type: A -> B

  • Behavior:

    • Every input event a → yields output f(a).
    • The output stream length strictly equals the input stream length.
  • Asynchrony: The output value is produced synchronously.

  • Characteristics: Simple and lightweight; it does not change the "shape" of the stream.

💡 Engineering Intuition: Use this for single-step synchronous conversions, such as Int -> String or Model -> ViewModel.


2️⃣ flatMap

Swift

sourcePublisher.flatMap { a in asyncPublisher(a) }
  • Essence: Maps one event into a new Publisher and "flattens" (merges) the events of all these internal Publishers into a single output stream.

  • Type: A -> Publisher<B> → yields B.

  • Behavior:

    • All internal Publishers are subscribed to in parallel.
    • Output events may arrive interleaved (out of order).
  • Asynchrony: Ideal for asynchronous tasks like network requests.

  • Characteristics:

    • Each input event triggers a new independent Publisher.
    • The output stream is a mixture of all active internal Publishers.
  • 🔥 Risk: High-frequency inputs → multiple parallel network requests → potential resource waste or concurrency chaos.

💡 Engineering Intuition: Use this when input events trigger independent async tasks that need to run in parallel and you require all results.


3️⃣ switchLatest (Combine) / flatMapLatest (Rx)

Swift

sourcePublisher
  .map { a in asyncPublisher(a) }
  .switchToLatest()
  • Essence: Switchable internal Publishers—retains only the output of the most recent internal Publisher.

  • Behavior:

    • New input event arrives → Cancels the previous internal Publisher.
    • Output strictly comes from the latest Publisher.
  • Asynchrony: Perfect for scenarios where only the result of the latest task matters.

  • Characteristics:

    • Prevents "stale" tasks from interfering with the UI.
    • Controls concurrent resource consumption.

💡 Engineering Intuition: Use this for high-frequency events (Search, Scrolling, Typing) where you only care about the latest result.


II. Behavioral Summary Table

OperatorInput → OutputConcurrencyRetains Old Events?Typical Scenario
map1:1 TransformationNoneN/AData mapping / Model → VM
flatMap1 → Publisher → Merged OutputParallelYesParallel API calls / Aggregating tasks
switchLatest1 → Publisher → Latest OutputSerial (Last one wins)No (Cancels old)Search-as-you-type / Latest UI state

III. Architectural Selection Principles

1️⃣ map → Synchronous / Derived State

  • Used for State → DerivedState or Model → ViewModel.
  • Does not alter the stream structure.
  • High performance and highly predictable.

2️⃣ map → Parallel Async Operations

  • Used when State triggers multiple tasks.

  • Retains all results → Handled by Reducers or Adapters.

  • Caution:

    • Control the concurrency limit (avoid flooding/race conditions).
    • The Reducer must be able to handle out-of-order data arrival.

3️⃣ switchLatest → High-Freq Events + Latest Task

  • Used for UI high-frequency events (Search bar, scrolling, throttled animations).
  • Automatically cancels old tasks → reduces resource overhead.
  • Integrates with State/Reducers to ensure UI Consistency (prevents old data from overwriting new data).

IV. Architectural Implementation Logic

Input Event → Transformation (map) → Async Task (flatMap/switch) → Reducer / Store
  • map: State derivation / ViewModel mapping.
  • flatMap: Parallel Effects / Networking / Database writes.
  • switchLatest: High-frequency input → Async request → Ensuring the latest UI state.

Core Principle: Choosing an Operator depends on Event Frequency + Historical Relevance + Parallel Processing Requirements.