在这里插入图片描述


本文目标:读完之后,你不仅能写出 lazy 链,还能在 10 行之内把它“拆”成零开销手写代码;更重要的是,你会明白什么时候该用 Iterator,什么时候该用 LazyCell,什么时候干脆自己写 enum 🦀


1. 开场:什么是“惰性”?

在 Rust 社区里,大家常说“迭代器是零成本的抽象”。
但“零成本”≠“零开销”——它只是意味着你不需要为你没用到的东西付费
惰性求值(Lazy Evaluation)正是这一哲学的核心:计算只在真正需要结果时才发生。

我们先看一个最朴素的例子:

// eager.rs
fn main() {
    let v = vec![1, 2, 3, 4, 5];
    let doubled: Vec<_> = v.iter().map(|x| x * 2).collect();
    println!("{:?}", doubled);
}

上面这段代码在 map 时就立即分配了一个新的 Vec
如果后续逻辑根本用不到 doubled,那我们就白分配了一次内存。
惰性版本长这样:

// lazy.rs
fn main() {
    let v = vec![1, 2, 3, 4, 5];
    let doubled = v.iter().map(|x| x * 2); // 类型是 Map<..., ...>
    // 这里没有任何内存分配!
    if std::env::args().len() > 1 {
        // 只有真正迭代时才执行 map 闭包
        for x in doubled {
            println!("{x}");
        }
    }
}

2. 从语法糖到 MIR:Iterator 惰性链剖析

2.1 为什么 Iterator 能“零成本”?

我们先写一个更复杂的链,然后把它“拆”成手写的状态机。

use std::iter;

fn main() {
    let data = [1i32, 2, 3, 4, 5];
    let res = data
        .iter()
        .copied()
        .filter(|&x| x % 2 == 0)
        .map(|x| x * x)
        .take(2)
        .sum::<i32>();
    println!("{res}");
}

编译器看到的 MIR(中间层)其实长这样(简化):

let mut iter = data.iter().copied();
let mut iter = Filter { iter, predicate: |&x| x % 2 == 0 };
let mut iter = Map { iter, f: |x| x * x };
let mut iter = Take { iter, n: 2 };
let mut acc = 0i32;
loop {
    match iter.next() {
        Some(x) => acc += x,
        None => break,
    }
}

注意:

  • 没有一次额外的堆分配;
  • 所有中间结构体都在栈上;
  • 每个 adaptor 都是一个零大小的 struct,仅携带必要的闭包指针。

这就是典型的“结构化惰性”:每一步都推迟到 next() 被调用时才发生。


2.2 自己手写一个 Map adaptor(实践 1)

为了验证上述说法,我们把 Map 从标准库里“抄”出来:

pub struct MyMap<I, F> {
    iter: I,
    f: F,
}

impl<I, F, B> Iterator for MyMap<I, F>
where
    I: Iterator,
    F: FnMut(I::Item) -> B,
{
    type Item = B;

    #[inline]
    fn next(&mut self) -> Option<Self::Item> {
        self.iter.next().map(&mut self.f)
    }

    #[inline]
    fn size_hint(&self) -> (usize, Option<usize>) {
        self.iter.size_hint()
    }
}

trait MyIteratorExt: Iterator {
    fn my_map<B, F>(self, f: F) -> MyMap<Self, F>
    where
        Self: Sized,
        F: FnMut(Self::Item) -> B,
    {
        MyMap { iter: self, f }
    }
}

impl<I: Iterator> MyIteratorExt for I {}

fn main() {
    let sum = (0..10)
        .my_map(|x| x * 2)
        .take(3)
        .sum::<i32>();
    println!("{sum}"); // 0+2+4 = 6
}

在 release 模式下,MyMap::next 会被 LLVM inline 成一条指令,最终汇编里只剩一个计数器加法。
这验证了:惰性 adaptor 链不仅语法优雅,还能真正跑成手写循环的速度


3. 当惰性遇到所有权:LazyCell 与 LazyLock

迭代器擅长“流式”惰性,但有时我们想对单个值做惰性初始化。
Rust 1.70 起,标准库引入了 std::sync::LazyLock(nightly 以前叫 once_cell::sync::Lazy)。

3.1 经典场景:读取环境变量一次,到处使用

use std::sync::LazyLock;

static CONFIG_PATH: LazyLock<String> = LazyLock::new(|| {
    std::env::var("CONFIG").unwrap_or_else(|_| "/etc/app/config.toml".into())
});

fn main() {
    println!("Config path = {}", *CONFIG_PATH);
    // 第二次访问不再执行闭包
    println!("Again = {}", *CONFIG_PATH);
}

3.2 深入源码:LazyLock 如何保证线程安全?

其核心结构:

pub struct LazyLock<T, F = fn() -> T> {
    cell: OnceLock<T>,
    init: Cell<Option<F>>,
}
  • OnceLock<T> 保证仅初始化一次
  • Cell<Option<F>> 允许在初始化完成后把闭包置空,避免长期持有资源;
  • 内部使用 atomic::Ordering 处理多线程竞争。

3.3 单线程版本:LazyCell(unstable)

如果你只在单线程使用,可以用 std::cell::LazyCell(nightly)或社区 crate once_cell::unsync::Lazy,性能更高:

#![feature(lazy_cell)]
use std::cell::LazyCell;

thread_local! {
    static HEAVY: LazyCell<String> = LazyCell::new(|| {
        std::fs::read_to_string("/tmp/big.txt").unwrap()
    });
}

fn main() {
    HEAVY.with(|s| println!("first 10 bytes: {:?}", &s[..10]));
}

4. 超越标准库:自研按需生成器(实践 2)

有时我们想表达无限的惰性序列,例如斐波那契数列,但不想写 Iterator 的四个方法。
Rust nightly 的 Generator API 可以派上用场。

4.1 手写一个 gen 宏(简化版)

#![feature(generators, generator_trait)]
use std::{
    ops::{Generator, GeneratorState},
    pin::Pin,
};

fn fib() -> impl Generator<Yield = u64, Return = ()> {
    || {
        let (mut a, mut b) = (0, 1);
        loop {
            yield a;
            (a, b) = (b, a + b);
        }
    }
}

fn main() {
    let mut g = Box::pin(fib());
    for _ in 0..10 {
        match g.as_mut().resume(()) {
            GeneratorState::Yielded(v) => println!("{v}"),
            GeneratorState::Complete(_) => break,
        }
    }
}

编译器会把生成器函数转成一个状态机 enum,每个 yield 变成一个状态转移。
这就是协程级别的惰性求值:计算暂停在 yield,直到外部再次 resume()


5. 性能与内存的权衡:什么时候不该惰性?

惰性并非银弹。下面列出几条“反模式”:

  1. 副作用泄漏
    如果 map 闭包里有日志或 I/O,惰性会让调用时机不可预测,导致调试困难。
    解决:把副作用提到 for 循环里,或者使用 inspect 并写文档。

  2. 栈深度爆炸
    很长的 iter().a().b().c()... 链在 debug 模式下会压很多层 next,可能触发栈溢出。
    解决:release 编译 + inline 提示,或手动 collect::<Vec<_>>() 到一个临时缓冲。

  3. 缓存命中率下降
    流式迭代对 CPU cache 不一定友好,尤其是随机访问。
    解决:使用 chunks_exactarray_chunks 提前批处理。


6. 综合实战:一个可扩展的惰性计算框架(实践 3)

我们来写一个“惰性表达式树”,支持 +*、变量替换,但只有在 eval() 时才真正计算。

use std::collections::HashMap;

#[derive(Clone)]
enum Expr {
    Num(i32),
    Var(String),
    Add(Box<Expr>, Box<Expr>),
    Mul(Box<Expr>, Box<Expr>),
}

impl Expr {
    // 关键:&self 不动,返回新树,保证共享子树
    fn subst(&self, env: &HashMap<String, i32>) -> Expr {
        match self {
            Expr::Num(n) => Expr::Num(*n),
            Expr::Var(name) => env.get(name).map(|&n| Expr::Num(n)).unwrap_or_else(|| self.clone()),
            Expr::Add(a, b) => Expr::Add(
                Box::new(a.subst(env)),
                Box::new(b.subst(env)),
            ),
            Expr::Mul(a, b) => Expr::Mul(
                Box::new(a.subst(env)),
                Box::new(b.subst(env)),
            ),
        }
    }

    fn eval(&self) -> i32 {
        match self {
            Expr::Num(n) => *n,
            Expr::Var(_) => panic!("unbound var"),
            Expr::Add(a, b) => a.eval() + b.eval(),
            Expr::Mul(a, b) => a.eval() * b.eval(),
        }
    }
}

fn main() {
    let expr = Expr::Add(
        Box::new(Expr::Mul(
            Box::new(Expr::Var("x".into())),
            Box::new(Expr::Num(2)),
        )),
        Box::new(Expr::Num(1)),
    );

    let mut env = HashMap::new();
    env.insert("x".into(), 10);

    let reduced = expr.subst(&env); // 惰性:没有真正计算
    println!("{}", reduced.eval()); // 这里才计算:21
}

你可以继续拓展:

  • subst 改成返回 Cow<Expr>,避免无意义克隆;
  • 加入 Lazy<Expr> 缓存节点,实现记忆化;
  • eval 换成 Iterator<Item = i32>,支持逐步求值。

7. 结语:把惰性思维写进 API 设计

Rust 的惰性体系并不是简单的“延迟计算”,而是一套从语言特性(ownership + move)、零大小类型、内联优化到生态 crate 的整体工程。

当你在设计库 API 时,可以问自己三个问题:

  1. 用户是否可能永远不需要这个值?
    → 考虑返回 impl Iterator 而非 Vec
  2. 这个值是否全局只需初始化一次
    → 使用 LazyLock
  3. 这个计算是否有副作用需严格控制时机
    → 直接求值或提供显式 build() 方法。

带着这些原则,你就能写出既优雅又高效的 Rust 代码。
祝你享受零成本抽象的魔法!🦀
在这里插入图片描述

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐