Rust中的闭包
Rust 的闭包在功能和用途上与其他语言的lambda相同,都是在本地定义的小型匿名函数,可以捕获外部变量。
基本语法
Rust 闭包的基本定义形式如下
|参数列表|:用竖线包裹参数,多个参数用逗号分隔(如|x, y|)。- 函数体:可以是单个表达式(省略
{}),也可以是多个语句(需用{}包裹)。
let closure_name = |参数列表| -> 返回类型 {表达式或语句块
};
类型与特征
闭包在 Rust 中有独立的匿名类型,并自动实现以下特征之一:
| Trait | 含义 | 适用场景 |
|---|---|---|
Fn | 不可变捕获环境(只读访问) | 可多次调用,捕获的变量仍可在外部使用 |
FnMut | 可变捕获环境(可修改捕获的变量) | 可多次调用,但捕获的变量在外部需为mut |
FnOnce | 转移捕获变量的所有权(move关键字) | 只能调用一次(所有权转移后无法再次使用) |
示例
pub fn closure_test() {let greeting = String::from("Hi");// Fn(只读)let say_hi = || println!("{}", greeting);say_hi();println!("greeting: {}", greeting);// FnMut(可变)let mut num = 0;let mut add_one = || num += 1;add_one();add_one();println!("num: {}", num);// FnOnce(移动)let s = String::from("Rust");let consume = move || {let tmp = s; // 闭包内字符串被移动到tmp,失去所有权println!("{}", tmp);};consume();// consume(); // 编译错误:闭包内捕获的s已被移动// println!("s: {}", s); // 编译错误:s已被移动
}
move语义
默认情况下,闭包会根据对变量的操作自动选择捕获方式(不可变借用、可变借用),尽量避免所有权转移。而move关键字会强制闭包 “夺取” 捕获变量的所有权(对于非Copy类型),或复制变量(对于Copy类型)。
- 非
Copy类型(如String,Vec)的所有权具有唯一性,move会让闭包直接获取其所有权,原变量在闭包外不可再使用(所有权已转移)。 Copy类型在赋值时会自动复制,move会让闭包获取变量的副本,原变量仍可正常使用。
move与闭包Trait
move仅决定闭包如何捕获变量(所有权转移),但闭包最终实现Fn/FnMut/FnOnce中的哪个 trait,取决于闭包如何使用捕获的变量(而非move本身)。
move + 只读使用 → 实现Fn
若move闭包仅只读访问捕获的变量(不修改、不消耗),则实现Fn trait,可多次调用。
let s = String::from("hello");
// move捕获s的所有权,闭包仅读取s
let closure = move || println!("{}", s);closure(); // 正常
closure(); // 正常(可多次调用,因未消耗s)
// println!("{}", s); // 编译错误:s的所有权已被转移到闭包中
move + 修改使用 → 实现FnMut
若move闭包修改捕获的变量(但不消耗),则实现FnMut trait,闭包自身需为mut。
////////////////////////////////////
// 非copy语义类型,所有权被转移
let mut v = vec![1, 2];
// move捕获v的所有权,闭包修改v
let mut closure = move || v.push(3);closure(); // v变为 [1,2,3]
closure(); // v变为 [1,2,3,3](可多次调用)
// println!("{}", v); // 编译错误:s的所有权已被转移到闭包中////////////////////////////////////
// copy语义类型,复制一个副本,修改不影响外部
let mut num = 0;
let mut add_one = move || {num += 1;println!("num: {}", num);
};
add_one(); // 1
add_one(); // 2
println!("num: {}", num); // 0
move + 消耗使用 → 实现FnOnce
若move闭包消耗捕获的变量(如调用drop或转移所有权给其他对象),则实现FnOnce trait,只能调用一次。
fn test_fnonce() {let s = String::from("hello");// move捕获s的所有权,闭包消耗s(转移给println!)let closure = move || {println!("{}", s); s // 这里s被消耗};closure(); // 正常// closure(); // 编译错误:s已被消耗,闭包只能调用一次
}
典型使用场景
move主要用于解决生命周期问题:
- 线程间传递闭包:线程的生命周期是独立的,无法保证外部变量的生命周期能覆盖线程的。此时
move可将变量所有权转移到线程内部,避免悬垂引用。 - 延长变量生命周期:当闭包需要 “带走” 变量并在更晚的时候使用时,
move可确保变量的所有权随闭包一起存在,避免提前销毁。
实现原理
闭包在编译时会被转换为一个匿名结构体类型(即使两个闭包代码完全相同,也会生成不同的类型)。结构体的字段为捕获的变量,实际字段类型取决于捕获方式(借用、所有权)。
- 若不捕获任何变量:结构体为零大小(ZST,无字段),不占用内存空间,调用成本极低(编译器可完全优化掉)。
- 若捕获多个变量:结构体字段按声明顺序连续存储(类似普通结构体)。
- 捕获引用时:结构体中仅存储变量的引用;
- 捕获所有权时:结构体存储被捕获变量的实际值(拥有所有权)。
闭包与对应的结构体:
let x = 10;
let y = String::from("hello");
let closure = |z| x + z; // 捕获x(不可变借用),不捕获y///////////////////////////////////////////////////
// 以上闭包会生成类似下面的
// 匿名结构体,命名为ClosureType仅作示例
struct ClosureType<'a> {x: &'a i32, // 捕获x的不可变引用(因仅读取x)
}impl<'a> Fn<(i32,)> for ClosureType<'a> {type Output = i32;// call方法的逻辑即闭包体:x + zfn call(&self, args: (i32,)) -> i32 {let (z,) = args; // 解构参数*self.x + z // 使用结构体字段x(不可变引用)}
}
