bigywater.tech
Rust · 65

flat_map 与嵌套数据展开

嵌套数据怎么展开?flat_map 与 flatten 有什么区别?内层闭包为什么经常要加 move?它惰性到什么程度?这篇把二维数组、拆词、Option、笛卡尔积的展开和惰性计数、size_hint、性能一次跑清楚。

E0373
内层闭包借用外层变量的错误
4 / 1000
take(7) 时实际创建的内层迭代器数 / 外层元素数
(0, None)
展开前 flatten 的 size_hint

展平与展开

实测 · 本机执行输出
  编译:成功
  运行(退出码 0):
    flat_map [1, 2, 3, 4, 5] / map+flatten [1, 2, 3, 4, 5] / flatten [1, 2, 3, 4, 5]
    按行拆词: ["to", "be", "or", "not", "to", "be"]
    字符: ['a', 'b', 'c', 'd']
    flat_map 把 Option 当作 0 或 1 个元素: [1, 3]
    一对多:每个数 n 展开成 1..=n: [1, 1, 2, 1, 2, 3]
    笛卡尔积: [(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b')]

惰性的基础见Iterator 的惰性执行。

内层闭包要 move

实测 · 本机执行输出
  源码:
      1 | fn main() {
      2 |     let v: Vec<(i32, i32)> = (1..=2).flat_map(|x| (1..=2).map(|y| (x, y))).collect();
      3 |     println!("{:?}", v);
      4 | }
  编译:失败
    error[E0373]: closure may outlive the current function, but it borrows `x`, which is owned by the current function
     --> <实验目录>/src/u65b.rs:2:63
      |
    2 |     let v: Vec<(i32, i32)> = (1..=2).flat_map(|x| (1..=2).map(|y| (x, y))).collect();
      |                                                               ^^^  - `x` is borrowed here
      |                                                               |
      |                                                               may outlive borrowed value `x`
      |
    note: closure is returned here
     --> <实验目录>/src/u65b.rs:2:51
      |
    2 |     let v: Vec<(i32, i32)> = (1..=2).flat_map(|x| (1..=2).map(|y| (x, y))).collect();
      |                                                   ^^^^^^^^^^^^^^^^^^^^^^^
    help: to force the closure to take ownership of `x` (and any other referenced variables), use the `move` keyword
      |
    2 |     let v: Vec<(i32, i32)> = (1..=2).flat_map(|x| (1..=2).map(move |y| (x, y))).collect();
      |                                                               ++++
    
    error: aborting due to 1 previous error
    
    For more information about this error, try `rustc --explain E0373`.

惰性

实测 · 本机执行输出
  编译:成功
  运行(退出码 0):
    take(7) = [0, 0, 1, 0, 1, 2, 0]
    内层迭代器创建了 4 个(外层有 1000 个),内层元素共产出 7 个

flatten 的类型细节

实测 · 本机执行输出
  编译:成功
  运行(退出码 0):
    rev: [6, 5, 4, 3, 2, 1]
    size_hint: flatten (0, None) / 单层 iter (3, Some(3))
    flatten 在没开始消费时 size_hint 的下界 0 是真实元素数 6 吗? 否:展开前不知道内层长度
    Result 展开丢弃 Err: [1, 3]

size_hint 对 collect 预分配的影响见collect 类型推断。

性能

实测 · 本机执行输出
  编译:成功
  运行(退出码 0):
    flat_map 链 0.154 ms  双重 for 0.156 ms  先对每行 sum 再 sum 0.153 ms  (结果 2570569 2570569 2570569)

数字是本机 release 实测;数据很小(1M 个 u32),不同形状(很多很短的内层)可能不同,没有测。

分步演示

flat_map
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / rustc 1.98.1(brew)/ cargo 1.98.1;单个文件用 rustc 直接编译,需要依赖时用 cargo(经代理从 crates.io 下载);计时用 std::time::Instant,数字每次运行会有小幅抖动。计时 release + black_box
    • 没有验证:很多极短内层迭代器的性能、flat_map 中返回 Vec(每次分配)与返回迭代器的差别、汇编层面是否与双重循环一致
    京ICP备2026067502号-1京公网安备11010502063487号