之前的文章:
本章主要介绍 rust 的闭包,包括与闭包相关的可变性、生命周期参数等问题。
闭包基础语法
与其他编程语言中的概念一致,“闭包”指的是一种简单的函数,它可以嵌套地写在其他函数中,并访问所在函数中的变量。在 rust 中的闭包基本语法如下例所示:
// 创建一个闭包let f = | /* 参数列表 */ | {// 代码};
多数情况下,闭包是传递给其他函数,作为“回调函数”来使用的。在传递闭包时,可以用一种特殊的参数类型 FnMut(...) 标示。例如:
fn for_each_item(v: &Vec<u32>,// 下面这个参数是闭包类型的// 它接收一个 u32 类型的参数// 且没有返回值mut f: impl FnMut(u32),) {for item in v {f(*item);}}fn main() {let v = vec![1, 3, 5, 7];let mut sum = 0;// 将一个闭包传递给这个函数// 闭包的参数类型和返回值类型可以省略for_each_item(&v, |item| {sum += item;});println!("{}", sum); // 输出 16}
闭包类型
具体而言,闭包的类型定义分为 FnOnce(...) 、 FnMut(...) 和 Fn(...) 三种。
FnOnce(...) 用于只被调用一次的闭包。在这样的闭包中,可以将闭包外变量的所有权转移进闭包内。例如:
struct MyStruct {n: u32,}fn for_the_first_item(v: &Vec<u32>,// 这里接受 FnOnce 类型的闭包f: impl FnOnce(u32),) {if v.len() >= 1 {f(v[0]);}}fn main() {let v = vec![2, 3, 5, 7];let s = MyStruct {n: 4,};let mut prod = 0;for_the_first_item(&v, |item| {// 对于 FnOnce 的闭包,// 可以将闭包外变量的所有权转移进来let t = s;prod = t.n * item;});println!("{}", prod); // 输出 8}
FnMut(...) 最为常见,是可以调用多次的闭包。在这样的闭包中,可以正常访问闭包外变量,但不能将闭包外变量的所有权转移进闭包内(因为闭包可能被执行多次,但所有权是唯一的)。
struct MyStruct {n: u32,}fn for_each_item(v: &Vec<u32>,// 这里接受 FnOnce 类型的闭包mut f: impl FnMut(u32),) {for item in v {f(*item);}}fn main() {let v = vec![2, 3, 5, 7];let s = MyStruct {n: 4,};let mut prod = 0;for_each_item(&v, |item| {// 对于 FnMut 的闭包,// 可以访问闭包外的变量但不能转移所有权,// 因而通常使用引用访问的方式let t = &s;prod += t.n * item;});println!("{}", prod); // 输出 68}
Fn(...) 是要求最为严格的闭包。与 FnMut(...) 的区别是,它只能不可变地访问闭包外变量,不能改变闭包外变量。通常,这样的闭包是“无状态的”,即调用时传递同样的参数就必然得到同样的结果。
struct MyStruct {n: u32,}fn for_each_item(v: &Vec<u32>,// 这里接受 Fn 类型的闭包// 这个闭包具有 u32 类型的返回值f: impl Fn(u32) -> u32,) -> u32 {let mut sum = 0;for item in v {sum += f(*item);}sum}fn main() {let v = vec![2, 3, 5, 7];let s = MyStruct {n: 4,};let prod = for_each_item(&v, |item| {// 对于 FnMut 的闭包,// 只能不可变地访问闭包外的变量let t = &s;// 闭包返回一个 u32 类型的值t.n * item});println!("{}", prod); // 输出 68}
通常,对于只执行一次闭包的情况,选用 FnOnce(...) ;需要执行多次的情况,选用 FnMut(...) ;Fn(...) 要求闭包是无状态的,在要求闭包有稳定的返回值时使用。
需要注意的是,闭包类型实际上都是一种特殊的 trait ,而非数据类型。因此需要在类型标注前加上 impl 。
静态生命周期闭包
有时,创建闭包时并不会马上调用它,而是把它存起来,在之后需要时调用。因为闭包类型实际上是 trait ,如果将闭包作为 struct 中的一个字段,常常需要用 boxed trait 的方式。例如:
struct Product {name: String,price: u32,// 使用 boxed trait 的方式存放一个闭包字段// 这个闭包具有 'static 生命周期final_price_fn: Box<dyn 'static + Fn(&Product) -> u32>,}
这样的闭包具有 'static 生命周期,即闭包不能访问闭包外的非 'static 引用。(这里的 'static 也可以省略不写,因为在这种情况下它是默认的生命周期。)
对于 'static 生命周期的闭包,闭包创建时通常需要加上 move ,表示将涉及到的变量所有权“移动到闭包环境中”。完整示例如下:
struct Product {name: String,price: u32,// 使用 boxed trait 的方式存放一个闭包字段// 这个闭包默认了 'static 生命周期final_price_fn: Box<dyn Fn(&Product) -> u32>,}impl Product {fn new(name: String, price: u32) -> Self {Self {name,price,final_price_fn: Box::new(|this| this.price),}}fn set_discount(&mut self, rate: f32) {// 对于 'static 生命周期的闭包,// 如果需要访问闭包外的变量,// 则需要加上 move ,表示将变量移动给闭包环境self.final_price_fn = Box::new(move |this| {(this.price as f32 * rate) as u32});}fn final_price(&self) -> u32 {let f = &self.final_price_fn;f(self)}}fn main() {let mut product = Product::new(format!("Geometry"),60,);product.set_discount(0.75);let final_price = product.final_price();println!("{}", final_price); // 输出 45}
从性能角度上看,闭包自身是零开销的;但结合 boxed trait 使用时,会具有与 boxed trait 近似的开销。
闭包是一种重要的抽象方式、在 rust 库中很常见,具体的用例将在之后的文章中介绍。




