跳到主要内容

闭包

问题

Rust 闭包的三种 Trait(FnFnMutFnOnce)有什么区别?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只能一次消耗捕获的值

继承关系:FnFnMutFnOnce。实现 Fn 的闭包自动实现 FnMutFnOnce

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?

答案

编译器根据闭包体内如何使用捕获的变量自动决定:

  1. 只读取 → Fn(不可变借用)
  2. 读取 + 修改 → FnMut(可变借用)
  3. 消耗/移出 → FnOnce(获取所有权)

编译器总是选择最宽松的方式——能借用就不 move。只有 move 关键字能强制获取所有权。

Q2: move 闭包和 FnOnce 是什么关系?

答案

它们是不同维度的概念:

  • move:决定如何获取外部变量(强制 move 所有权进闭包)
  • FnOnce/FnMut/Fn:决定闭包如何被调用(由闭包体内的使用方式决定)

一个 move 闭包可以是 FnFnMutFnOnce

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: 函数参数应该接受 FnFnMut 还是 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 FnBox<dyn Fn>

相关链接