3、Flow操作符(上):map、filter、transform、take、drop等基础操作符

Flow 操作符,说白了就是帮你处理数据流的「管道工」。你从网络或数据库拿到一串数据,总不能直接往 UI 上怼吧?中间总得洗洗涮涮、挑挑拣拣。我个人习惯把操作符分成两类:一类是「冷操作符」,像 map、filter 这些,它们不触发数据发射;另一类是「终端操作符」,比如 collect,一调用数据就开始跑了。今天咱们先聊第一类——基础转换操作符。

核心要点:Flow 操作符是冷操作,只有在调用 collect 等终端操作符时才会真正执行。操作符之间是链式调用,每个操作符都会返回一个新的 Flow。

3.1 map:一对一转换

map 是最常用的操作符。它的作用很简单:把 Flow 里的每个元素,通过你提供的函数,变成另一个东西。

我在项目中遇到过这样一个场景:后端返回的用户 ID 是 Long 类型,但 UI 层需要展示的是格式化后的字符串。用 map 就特别顺手:

val userIdFlow: Flow<Long> = ...

val displayFlow = userIdFlow.map { id ->
    "用户 #$id"
}

嗯,这里要注意:map 是「一对一」的。一个输入,一个输出。如果你想让一个输入变成多个输出,那就得用别的操作符了。

3.2 filter:数据筛选器

filter 就是字面意思——过滤。它接收一个返回 Boolean 的 lambda,只有返回 true 的元素才会继续往下传。

我记得有一次做日志分析,数据流里混了大量调试日志,生产环境根本不需要。用 filter 一筛,干净利落:

val logFlow: Flow<LogEntry> = ...

val errorFlow = logFlow.filter { entry ->
    entry.level >= LogLevel.ERROR
}

filter 不会改变数据类型,它只是决定「放行」还是「拦截」。你想想看,这跟现实中的安检流程一模一样——人还是那个人,只是决定让不让你过去。

3.3 transform:万能转换器

transform 是我个人比较偏爱的操作符。它比 map 灵活得多。map 只能一对一,但 transform 可以发射零个、一个或多个元素。

为什么需要这个?举个例子。你从数据库查到的用户数据,可能包含已删除的用户。用 map 你没法跳过,用 filter 你得先过滤再转换。但 transform 一步到位:

val userFlow: Flow<User> = ...

val displayFlow = userFlow.transform { user ->
    if (user.isDeleted) {
        // 跳过已删除用户,什么都不发射
        return@transform
    }
    // 发射转换后的数据
    emit("${user.name} (${user.id})")
    // 还可以发射额外信息
    emit("--- 分隔符 ---")
}

看到没?transform 里你可以自由控制 emit 的次数。我曾经用它来实现数据流中的「心跳包」——每处理 10 条数据就额外发射一个状态标记。map 做不到这点。

小技巧:transform 内部可以调用多次 emit,也可以不调用。这让你能实现「一对零」、「一对多」的转换逻辑。但别滥用,保持代码可读性更重要。

3.4 take 与 drop:截取与跳过

这两个操作符是控制数据量的好帮手。take(n) 表示只取前 n 个元素,drop(n) 表示跳过前 n 个元素。

我做过一个实时搜索功能,用户输入关键词后,后端会返回大量建议结果。但 UI 上只需要展示前 5 条。用 take 就搞定了:

val suggestionFlow: Flow<String> = ...

val top5Flow = suggestionFlow.take(5)

drop 的用法类似。比如你想忽略数据流的前 3 条「预热数据」,只处理后面的稳定数据:

val sensorDataFlow: Flow<Double> = ...

val stableFlow = sensorDataFlow.drop(3)

这里有个坑:take 和 drop 都是「有状态」的操作符。它们内部会记录已经处理了多少个元素。如果你在多个 collect 中复用同一个 Flow,结果可能不符合预期。我曾经因为这个排查了半天——同一个 Flow,第一次 collect 正常,第二次 collect 数据全没了。原因就是 take 的状态被共享了。

避坑指南:take 和 drop 是冷操作符,但它们的「计数状态」是在每次 collect 时重新初始化的。如果你用 stateIn 或 shareIn 把 Flow 变成热流,那就要小心了——状态可能会被多个订阅者共享。

3.5 操作符组合使用

实际项目中,很少只用单个操作符。更多时候是把它们串起来用。比如:

val rawDataFlow: Flow<String> = ...

val processedFlow = rawDataFlow
    .filter { it.isNotBlank() }          // 过滤空字符串
    .map { it.trim().toIntOrNull() }     // 转成 Int?
    .filterNotNull()                     // 过滤转换失败的
    .take(10)                            // 只取前 10 个有效数据
    .transform { value ->
        emit("数值: $value")
        if (value > 100) {
            emit("⚠️ 超过阈值!")
        }
    }

你想想看,这个管道就像一条生产线:先筛掉不合格的原料,再加工成半成品,剔除次品,只保留前 10 件成品,最后给每件成品贴上标签。每个操作符各司其职,清晰明了。

3.6 知识体系总览

下面这张图总结了本章涉及的操作符及其关系:

Flow 基础操作符知识体系 原始数据流 操作符链(冷操作,不触发执行) map 一对一转换 filter 条件筛选 transform 零到多发射 take / drop 截取 / 跳过 collect 触发执行 操作符链不会执行,直到调用 collect 等终端操作符

3.7 性能与注意事项

操作符用起来很爽,但有些细节你得心里有数:

操作符 是否改变数据类型 是否改变元素数量 是否有内部状态
map 不变(一对一)
filter 可能减少
transform 可变(零到多)
take 减少(最多 n 个) 是(计数)
drop 减少(跳过前 n 个) 是(计数)

我个人建议:能用 map 和 filter 解决的问题,就别上 transform。代码越简单,越不容易出错。transform 虽然强大,但用多了会让管道变得难以追踪。

另外,操作符链中的每个操作符都会创建一个新的 Flow 对象。如果你链了十几个操作符,每次 collect 都会重新走一遍整个管道。这在数据量小时没问题,但如果数据量巨大(比如每秒几千条),就要考虑用 buffer 或 conflate 来优化背压了——那是后面章节的内容。

经验之谈:我一般把操作符链控制在 5 个以内。超过这个数,我会考虑把部分逻辑抽成扩展函数,或者用 combine 等操作符合并多个 Flow。代码可读性比炫技重要得多。

好了,基础操作符就聊到这儿。map、filter、transform、take、drop 这五个操作符,覆盖了日常开发中 80% 的数据转换场景。下一节我们会聊更高级的操作符——flatMapConcat、flatMapMerge、zip、combine 这些。嗯,到时候你会发现,Flow 的玩法比你想的还要多。


公众号:蓝海资料掘金营,微信deep3321