
从闭包捕获与 Fn/FnMut/FnOnce 讲到迭代器的惰性求值说清链条为什么会被编译成一条循环上一篇讲dyn Trait时我们把具体类型什么时候确定这个问题摊开算了一遍账。现在换一个方向从这一篇开始转向用起来顺手的那一侧。Rust 里到处都是这样的代码lettotal:u32items.iter().filter(|x|x.price100).map(|x|x.price).sum();写惯了 Java Stream 或 Python 生成器的人第一反应是这写法很眼熟Rust中这种写法叫闭包。闭包是一个装着捕获变量的匿名结构体闭包的语法你已经不陌生|x| x 1。它和普通函数的区别只有一条——闭包能捕获定义它的作用域里的变量。所以unwrap_or_else(|| self.most_stocked())能工作而一个普通函数做不到这件事因为标准库根本不知道Inventory这个类型的存在。捕获方式有三种The Rust Book 的措辞值得照抄闭包捕获值的方式正好对应函数接收参数的三种方式——不可变借用、可变借用、取得所有权具体用哪一种由闭包体对这些值做了什么决定。letlistvec![1,2,3];letonly_read||println!({:?},list);// 不可变借用letmutpushes||list.push(4);// 可变借用 → 需要 let mut pushes第二行里闭包体要改list所以它捕获的是list的可变引用。这直接触发你在借用篇学过的那条规则闭包定义之后、被调用之前list不能再被别的借用碰因为可变借用是独占的。Book 特意提醒了一个新手常踩的坑——在闭包定义和调用之间插一句println!({:?}, list)会编译不过原因就是此时list正被可变借用。换个角度看会更清楚。Rust Reference 说闭包类型大致等价于一个装着捕获值的结构体。它给的示意大致是这样——一个捕获了rect.left_top整体、以及rect.right_bottom.x这一个字段的闭包会被翻译成structClosurea{left_top:amutPoint,right_bottom_x:amuti32,// 只捕获了一个字段不是整个 Point}这不是类比是编译器实际做的事闭包是个普通的 struct捕获的值就是它的字段调用它等于调用这个 struct 上的一个方法。既然它是个 struct它当然是一个具体类型——而具体类型意味着后面可以单态化这一点是整篇的伏笔。Fn、FnMut、FnOnce由做了什么决定而不是怎么捕获闭包类型不止一个字段的形状它还自动实现若干 trait。规则是叠加的所有闭包都实现FnOnce——任何闭包至少能被调用一次。闭包体不把捕获值移出去就再实现FnMut表示可以被可变借用着反复调用。既不移动也不修改捕获值就再实现Fn表示可以被共享引用反复调用。三个名字读起来别扭换成参数类型就直白了Fn相当于闭包内部只拿着TFnMut拿着mut TFnOnce拿着T。和之前篇中的self/mut self/self是同一套层次只不过这里约束的是闭包本身。为什么标准库要区分这三者因为签名要如实表达我会调用你几次。Option::unwrap_or_else的签名是F: FnOnce() - T原因是它最多只调一次——Some的情况下根本不会调fNone时才调一次。用FnOnce当约束是这里最宽松、最灵活的选择。反过来slice::sort_by_key的约束是FnMut因为它对每个元素都调一次闭包必须能反复调。这就是判断标准的全部接收方要调用几次就用哪个 bound。这里有个反直觉的点值得单独说因为很多人第一次都会猜错move不改变闭包实现哪个 trait。Rust Reference 的原话是——trait 由闭包对捕获值做了什么决定而不是它怎么捕获。所以一个move闭包完全可以是FnletsString::from(hi);letfmove||println!({s});// 按值捕获 → 仍是 Fn、FnMut、FnOnce 全实现f();f();// 能调两次因为闭包里只读了 s没有把它移出去move管的是捕获时把值搬进闭包不是闭包只能用一次。它真正的用途是让闭包活得比定义它的作用域更久——返回值里的闭包、传给thread::spawn的闭包都需要它。迭代器是惰性的状态机Iterator这个 trait 你已经熟悉了。上一篇讲过它只要求实现一个方法pubtraitIterator{typeItem;fnnext(mutself)-OptionSelf::Item;}其余几百个方法全是默认实现统统建立在next上。所以有了next你就白拿map、filter、zip、sum——这是 trait 篇那条规则的直接后果。接下来的行为是理解一切的关键在 Rust 中迭代器是惰性的除非你调用会消耗它的方法把它用掉否则它什么效果也没有。letvvec![1,2,3];v.iter().map(|x|x1);// 什么都不发生这行代码编译时会给出unused Map that must be used警告——注意这个提示的机制就是错误处理篇里Result上那个#[must_use]。警告产生的原因是map这样的适配器adapter不消耗迭代器而是返回一个新的迭代器真正推进计算的只有sum、collect、for这类消耗型适配器consuming adapter。所以map里那个闭包在上面这段代码里从来没被调用过。for循环是这个机制最常见的外壳。它隐含地创建并消耗一个迭代器反复调next直到None。所以下面两种写法是同一件事forxinv{println!({x});}letmutitv.iter();whileletSome(x)it.next(){println!({x});}顺便解决三个经常被混淆的入口它们的区别正好是所有权篇讲过的三种关系iter()产生T不可变借用、iter_mut()产生mut T可变借用、into_iter()产生T拿走所有权。所以for x in v和for x in v在所有权上的含义完全不同。链条为什么会被编译成一条循环现在把前面两块拼起来。items.iter().filter(...).map(...).sum()在源码里是三个嵌套的抽象但对编译器来说闭包是具体的匿名类型迭代器适配器也是具体的结构体FilterMapIter_, Item, C1, C2这种嵌套类型全部是编译期已知的具体类型。于是泛型篇讲过的单态化在这里接管一切每个具体类型生成一份专门代码next调用被内联闭包的调用也被内联——调到再也不能调为止。链条塌缩之后剩下的就是一个循环。迭代器虽然是高层抽象但编译出来的代码和你自己写的低层代码大致相同是 Rust 的零成本抽象之一使用这种抽象不带来额外的运行时开销。和 Java、Python 对一下账Java 的 Stream 同样惰性中间操作不执行遇到终止操作才跑。所以惰性不是 Rust 独有的。差别在每一级链条的成本构成上Stream 的每一级是一个接口调用运行时按实际类型分发流里的元素是对象引用遍历int要么装箱成Integer除非用IntStream。JIT 的热点优化能把相当一部分补回来但那取决于运行时的类型画像属于可能被优化掉而不是编译期给定的结果。Python 更彻底。生成器确实做到了惰性与增量PEP 255 引入的yield就是为这件事设计的但 CPython 里每个生成器是一个持有自己帧对象的对象每次yield都要从求值循环里出来、再进去类型错误只能在运行时显现。链式写法并没有错只是每一层都要经过解释器。Rust 的闭包和迭代器代价是有的——它照旧从运行时挪到了编译期多一份单态化拷贝就多一份编译时间和二进制体积这就是泛型篇末尾提到的那笔账嵌入式固件里同一个方法出现 59 份拷贝。下一篇换到工程结构那一侧当代码不再是单个main.rs模块mod和包crate怎么组织pub的边界又该怎么划。参考来源The Rust Book 第 13.1 章「Closures」— 三种捕获方式对应函数的三种参数形式、可变借用期间的借用冲突、Fn/FnMut/FnOnce 的叠加关系与 unwrap_or_else、sort_by_key 的 bound 选择https://doc.rust-lang.org/book/ch13-01-closures.htmlThe Rust Reference: Closure types — 闭包类型约等于装着捕获值的结构体、四种捕获模式、精确捕获路径与 Edition 2021 的变化、move 不改变实现的 traithttps://doc.rust-lang.org/stable/reference/types/closure.htmlThe Rust Book 第 13.2 章「Processing a Series of Items with Iterators」— Iterator 只需实现 next、惰性求值、for 循环的隐含消耗、iter/iter_mut/into_iter 的区别、map 未消耗时的警告https://doc.rust-lang.org/book/ch13-02-iterators.htmlThe Rust Book 第 13.4 章「Comparing Performance: Loops vs. Iterators」— 福尔摩斯全文搜索的 benchmark 数据、循环展开与边界检查消除、Stroustrup 的零开销原则引文https://doc.rust-lang.org/book/ch13-04-performance.htmlstd::ops::Fn 文档 — Fn 的自动实现条件、FnMut/FnOnce 是它的父 trait、何时该用哪个作为约束https://doc.rust-lang.org/std/ops/trait.Fn.htmlRust By Example「Closures as input parameters」— Fn/FnMut/FnOnce 按限制递减排列及各自的捕获含义https://doc.rust-lang.org/rust-by-example/fn/closures/input_parameters.html