闭包
问题
Rust 闭包的三种 Trait(Fn、FnMut、FnOnce)有什么区别?move 关键字的作用是什么?
答案
闭包(Closure)是可以捕获外部环境变量的匿名函数。Rust 的闭包与所有权系统紧密结合——闭包如何捕获变量(借用还是获取所有权)决定了它实现哪种 Trait。
基本语法
fn main() {
// 完整写法
let add = |x: i32, y: i32| -> i32 { x + y };
// 简写(编译器推断类型和返回值)
let add = |x, y| x + y;
// 无参数
let greet = || println!("hello");
// 多行
let process = |x: i32| {
let doubled = x * 2;
doubled + 1
};
println!("{}", add(1, 2)); // 3
greet(); // hello
println!("{}", process(5)); // 11
}
三种闭包 Trait
闭包的捕获方式决定了它实现哪种 Trait:
| Trait | 捕获方式 | 调用所需 | 可调用次数 | 典型场景 |
|---|---|---|---|---|
Fn | 不可变借用 &T | &self | 无限次 | 读取环境,不修改 |
FnMut | 可变借用 &mut T | &mut self | 无限次 | 需要修改捕获的变量 |
FnOnce | 获取所有权(move) | self | 只能一次 | 消耗捕获的值 |
继承关系:Fn ⊂ FnMut ⊂ FnOnce。实现 Fn 的闭包自动实现 FnMut 和 FnOnce。
Fn — 不可变借用捕获
fn main() {
let name = String::from("Alice");
// 闭包只读取 name,编译器选择 &name(不可变借用)
let greet = || println!("Hello, {}!", name);
greet(); // ✅ 可以多次调用
greet(); // ✅
println!("{}", name); // ✅ name 未被 move
}
FnMut — 可变借用捕获
fn main() {
let mut count = 0;
// 闭包修改 count,编译器选择 &mut count
let mut increment = || {
count += 1;
println!("count = {}", count);
};
increment(); // count = 1
increment(); // count = 2
// println!("{}", count); // ❌ 闭包可变借用还活着时不能再借用
drop(increment); // 显式释放闭包
println!("{}", count); // ✅ count = 2
}
FnOnce — 获取所有权
fn main() {
let name = String::from("Alice");
// 闭包消耗了 name(drop 需要所有权)
let consume = || {
drop(name); // name 的所有权被消耗
println!("名字已释放");
};
consume(); // ✅ 第一次调用
// consume(); // ❌ 编译错误:闭包已消耗捕获的值,不能再调用
}
move 关键字
move 强制闭包获取所有权,即使闭包体内只需要引用:
fn main() {
let name = String::from("Alice");
// 没有 move:闭包借用 name
let greet = || println!("{}", name);
println!("{}", name); // ✅ name 未被 move
// 有 move:闭包获取 name 的所有权
let greet = move || println!("{}", name);
// println!("{}", name); // ❌ name 已被 move 进闭包
}
move 不改变捕获方式的 Trait 分类
move 只决定"如何获取外部变量",不决定闭包实现哪种 Trait。如果闭包内只读取 move 进来的值,它仍然实现 Fn:
let name = String::from("Alice");
let greet = move || println!("{}", name);
// greet 实现了 Fn(虽然 name 被 move 进来,但闭包体只读取)
greet(); // ✅
greet(); // ✅ 可以多次调用
move 在线程中的应用
move 最常见的场景是把数据发送到新线程:
use std::thread;
fn main() {
let data = vec![1, 2, 3];
// 必须 move,因为新线程可能比当前函数活得更久
let handle = thread::spawn(move || {
println!("{:?}", data);
});
// println!("{:?}", data); // ❌ data 已被 move
handle.join().unwrap();
}
闭包作为函数参数
// 接受 Fn 闭包(最宽松,调用方可以传入任何闭包)
fn apply<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 {
f(x)
}
// 接受 FnMut 闭包
fn apply_mut<F: FnMut()>(mut f: F) {
f();
f();
}
// 接受 FnOnce 闭包
fn apply_once<F: FnOnce() -> String>(f: F) -> String {
f() // 只调用一次
}
闭包作为返回值
// 返回闭包必须用 Box<dyn Fn> 或 impl Fn
fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
move |y| x + y // 必须 move,否则 x 在函数结束后被释放
}
fn main() {
let add5 = make_adder(5);
println!("{}", add5(3)); // 8
}
闭包与函数指针
函数指针 fn 是闭包 Trait 的子集——不捕获环境的闭包可以当函数指针用:
fn add_one(x: i32) -> i32 { x + 1 }
fn apply(f: fn(i32) -> i32, x: i32) -> i32 { f(x) }
fn main() {
apply(add_one, 5); // ✅ 函数指针
apply(|x| x + 1, 5); // ✅ 不捕获环境的闭包也兼容 fn
}
常见面试问题
Q1: 编译器如何决定闭包实现哪种 Trait?
答案:
编译器根据闭包体内如何使用捕获的变量自动决定:
- 只读取 →
Fn(不可变借用) - 读取 + 修改 →
FnMut(可变借用) - 消耗/移出 →
FnOnce(获取所有权)
编译器总是选择最宽松的方式——能借用就不 move。只有 move 关键字能强制获取所有权。
Q2: move 闭包和 FnOnce 是什么关系?
答案:
它们是不同维度的概念:
move:决定如何获取外部变量(强制 move 所有权进闭包)FnOnce/FnMut/Fn:决定闭包如何被调用(由闭包体内的使用方式决定)
一个 move 闭包可以是 Fn、FnMut 或 FnOnce:
let s = String::from("hi");
let f = move || println!("{}", s); // move 但只读取 → Fn
f(); f(); // ✅ 可多次调用
let s = String::from("hi");
let f = move || drop(s); // move 且消耗 → FnOnce
f(); // ✅
// f(); // ❌ 只能调用一次
Q3: 为什么 thread::spawn 要求 move 闭包?
答案:
thread::spawn 的签名要求闭包是 'static + Send:
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,
新线程的生命周期是不确定的——它可能比创建它的函数活得更久。如果闭包只是借用局部变量,这些变量可能在线程还在运行时就被释放了(悬垂引用)。因此必须 move 所有权到线程中,确保线程拥有自己的数据。
Q4: 函数参数应该接受 Fn、FnMut 还是 FnOnce?
答案:
优先选择约束最宽松的,让调用方有更大灵活性:
- 只调用一次 →
FnOnce(最宽松,接受所有闭包) - 可能调用多次,但不需要并发共享 →
FnMut - 可能调用多次,且需要并发共享 →
Fn
// Iterator::map 接受 FnMut(因为每个元素调用一次,但要多次调用)
fn map<B, F>(self, f: F) -> Map<Self, F> where F: FnMut(Self::Item) -> B;
// thread::spawn 接受 FnOnce(线程只启动一次)
fn spawn<F: FnOnce() + Send + 'static>(f: F) -> JoinHandle<()>;
Q5: 闭包的大小是多少?
答案:
闭包的大小取决于捕获了什么:
let a = 42_i32;
let b = String::from("hello");
let c1 = || {}; // 捕获空,0 字节
let c2 = || println!("{}", a); // 捕获 &i32,8 字节(引用大小)
let c3 = move || println!("{}", a); // 捕获 i32,4 字节(值大小)
let c4 = move || drop(b); // 捕获 String,24 字节(ptr+len+cap)
每个闭包都是唯一的匿名类型,即使两个闭包签名一样,它们也是不同类型。这就是为什么返回闭包需要 impl Fn 或 Box<dyn Fn>。