☰
闭包与迭代器:函数式写法
2026/10/2 20:39:52 网站建设 项目流程

从闭包捕获与 Fn/FnMut/FnOnce 讲到迭代器的惰性求值,说清链条为什么会被编译成一条循环

上一篇讲dyn Trait时,我们把"具体类型什么时候确定"这个问题摊开算了一遍账。现在换一个方向:从这一篇开始转向"用起来顺手"的那一侧。

Rust 里到处都是这样的代码:

lettotal:u32=items.iter().filter(|x|x.price>100).map(|x|x.price).sum();

写惯了 Java Stream 或 Python 生成器的人,第一反应是"这写法很眼熟",Rust中这种写法叫闭包。

闭包是一个装着捕获变量的匿名结构体

闭包的语法你已经不陌生:|x| x + 1。它和普通函数的区别只有一条——闭包能捕获定义它的作用域里的变量。所以unwrap_or_else(|| self.most_stocked())能工作,而一个普通函数做不到这件事,因为标准库根本不知道Inventory这个类型的存在。

捕获方式有三种,The Rust Book 的措辞值得照抄:闭包捕获值的方式正好对应函数接收参数的三种方式——不可变借用、可变借用、取得所有权,具体用哪一种,由闭包体对这些值做了什么决定。

letlist=vec![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这一个字段的闭包,会被翻译成:

structClosure<'a>{left_top:&'amutPoint,right_bottom_x:&'amuti32,// 只捕获了一个字段,不是整个 Point}

这不是类比,是编译器实际做的事:闭包是个普通的 struct,捕获的值就是它的字段,调用它等于调用这个 struct 上的一个方法。既然它是个 struct,它当然是一个具体类型——而具体类型意味着后面可以单态化,这一点是整篇的伏笔。

Fn、FnMut、FnOnce:由"做了什么"决定,而不是"怎么捕获"

闭包类型不止一个字段的形状,它还自动实现若干 trait。规则是叠加的:

  • 所有闭包都实现FnOnce——任何闭包至少能被调用一次。
  • 闭包体不把捕获值移出去,就再实现FnMut,表示可以被可变借用着反复调用。
  • 既不移动也不修改捕获值,就再实现Fn,表示可以被共享引用反复调用。

三个名字读起来别扭,换成参数类型就直白了:Fn相当于闭包内部只拿着&T,FnMut拿着&mut T,FnOnce拿着T。和之前篇中的&self/&mut self/self是同一套层次,只不过这里约束的是闭包本身。

为什么标准库要区分这三者?因为签名要如实表达"我会调用你几次"。Option::unwrap_or_else的签名是F: FnOnce() -> T,原因是它最多只调一次——Some的情况下根本不会调f,None时才调一次。用FnOnce当约束是这里最宽松、最灵活的选择。反过来,slice::sort_by_key的约束是FnMut,因为它对每个元素都调一次闭包,必须能反复调。这就是判断标准的全部:接收方要调用几次,就用哪个 bound。

这里有个反直觉的点值得单独说,因为很多人第一次都会猜错:move不改变闭包实现哪个 trait。Rust Reference 的原话是——trait 由闭包对捕获值做了什么决定,而不是它怎么捕获。所以一个move闭包完全可以是Fn:

lets=String::from("hi");letf=move||println!("{s}");// 按值捕获 → 仍是 Fn、FnMut、FnOnce 全实现f();f();// 能调两次,因为闭包里只读了 s,没有把它移出去

move管的是"捕获时把值搬进闭包",不是"闭包只能用一次"。它真正的用途是让闭包活得比定义它的作用域更久——返回值里的闭包、传给thread::spawn的闭包,都需要它。

迭代器是惰性的状态机

Iterator这个 trait 你已经熟悉了。上一篇讲过它只要求实现一个方法:

pubtraitIterator{typeItem;fnnext(&mutself)->Option<Self::Item>;}

其余几百个方法全是默认实现,统统建立在next上。所以有了next,你就白拿map、filter、zip、sum——这是 trait 篇那条规则的直接后果。

接下来的行为是理解一切的关键:在 Rust 中,迭代器是惰性的,除非你调用会消耗它的方法把它用掉,否则它什么效果也没有。

letv=vec![1,2,3];v.iter().map(|x|x+1);// 什么都不发生

这行代码编译时会给出unused Map that must be used警告——注意这个提示的机制,就是错误处理篇里Result上那个#[must_use]。警告产生的原因是:map这样的适配器(adapter)不消耗迭代器,而是返回一个新的迭代器;真正推进计算的只有sum、collect、for这类消耗型适配器(consuming adapter)。所以map里那个闭包在上面这段代码里从来没被调用过。

for循环是这个机制最常见的外壳。它隐含地创建并消耗一个迭代器,反复调next直到None。所以下面两种写法是同一件事:

forxin&v{println!("{x}");}letmutit=v.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()在源码里是三个嵌套的抽象,但对编译器来说:闭包是具体的匿名类型,迭代器适配器也是具体的结构体(Filter<Map<Iter<'_, 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.html
  • The Rust Reference: Closure types — 闭包类型约等于装着捕获值的结构体、四种捕获模式、精确捕获路径与 Edition 2021 的变化、move 不改变实现的 trait:https://doc.rust-lang.org/stable/reference/types/closure.html
  • The 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.html
  • The Rust Book 第 13.4 章「Comparing Performance: Loops vs. Iterators」— 福尔摩斯全文搜索的 benchmark 数据、循环展开与边界检查消除、Stroustrup 的零开销原则引文:https://doc.rust-lang.org/book/ch13-04-performance.html
  • std::ops::Fn 文档 — Fn 的自动实现条件、FnMut/FnOnce 是它的父 trait、何时该用哪个作为约束:https://doc.rust-lang.org/std/ops/trait.Fn.html
  • Rust By Example「Closures as input parameters」— Fn/FnMut/FnOnce 按限制递减排列及各自的捕获含义:https://doc.rust-lang.org/rust-by-example/fn/closures/input_parameters.html

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询