Rust 函数与闭包:捕获环境的匿名函数
更新时间:2026-09-20。闭包是「能捕获周围变量的匿名函数」,也是迭代器适配器的基础。所有权捕获规则与所有权与借用完全一致。
一、普通函数与函数指针
rust
fn add(a: i32, b: i32) -> i32 { a + b }
let f: fn(i32, i32) -> i32 = add; // 函数指针类型
let r = f(1, 2);普通 fn 不捕获环境,可以转成函数指针 fn(...) -> ...;闭包因为可能捕获环境,不一定能退化成函数指针。
二、闭包的写法与类型推断
rust
let square = |x| x * x; // 参数/返回类型通常由调用处推断
let n = square(5);
let add = |a: i32, b: i32| -> i32 { a + b }; // 也可显式标注闭包参数类型在第一次调用时被锁定;同一闭包不能先用 i32 调又用 String 调。
三、三种捕获方式与 move
闭包默认按「尽量小」的代价捕获环境:先尝试不可变借用,再可变借用,最后才移动。
rust
let list = vec![1, 2, 3];
let contains = |x| list.contains(&x); // 只按 &list 不可变借用
println!("{:?}", list); // 借用期间规则同普通引用
let mut count = 0;
let mut bump = || { count += 1; }; // 需要 &mut count
bump();
println!("{count}");
let data = vec![1, 2];
let take = move || println!("{:?}", data); // move:所有权搬进闭包
take();
// println!("{:?}", data); // data 已被 move,这里报错move 常用于把闭包丢进线程/异步任务,让它自给自足、不借用外部栈变量。
四、Fn / FnMut / FnOnce
| trait | 调用对捕获物的影响 | 典型 |
|---|---|---|
FnOnce | 可能消耗捕获物,只能调用一次 | move 出捕获值、drop |
FnMut | 可变借用捕获物,可多次调用并改状态 | 计数器、累加器 |
Fn | 不可变借用,可反复并发调用 | 纯查询、映射 |
rust
fn run<F: FnOnce()>(f: F) { f(); } // 最宽:所有闭包至少能调用一次
fn call_twice<F: FnMut()>(mut f: F) { f(); f(); }
fn shared<F: Fn()>(f: F) { f(); f(); } // 最严:不改变捕获关系是 Fn: FnMut: FnOnce:能反复只读调用的 Fn 自然也满足 FnMut/FnOnce。接收闭包参数时按实际需求选最窄的约束。
五、常见坑
- move 后继续用原变量:所有权已进闭包,外部变量失效;只想借用就别加
move。 - 闭包当 Fn 却改了状态:修改捕获变量需要
FnMut,变量本身也要let mut。 - 循环中捕获循环变量:多线程场景每个任务应
move自己的副本,避免共享同一个可变引用。 - 把闭包存进字段:闭包是匿名且大小不定的类型,要用泛型
F: Fn(..)或Box<dyn Fn(..)>(动态分发见 trait)。 - 期望类型推断通用:闭包参数类型一次调用后固定,需要泛型行为应改成普通泛型函数。
总结
普通 fn 不捕获环境、可转为函数指针,闭包则是按最小代价(不可变借用→可变借用→移动)捕获环境的匿名函数,move 可强制把捕获变量的所有权搬进闭包以满足线程和 'static 需求;闭包能力分 FnOnce(可能消耗捕获、只调一次)、FnMut(可变借用、能多次改状态)、Fn(不可变借用、可反复调用),且 Fn 自动满足后两者;参数按最窄需求选 trait,存储匿名闭包要用泛型或 Box<dyn Trait>,并注意 move 后原变量失效、修改状态需 FnMut、循环中给每个任务独立副本。