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 知识体系总览
下面这张图总结了本章涉及的操作符及其关系:
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