Rust —— 闭包

闭包(Closure)是 Rust 中一种可以捕获环境变量的匿名函数。与普通函数不同,闭包可以保存在变量中、作为参数传递给其他函数,并且能够捕获定义时所在作用域的变量。闭包是 Rust 函数式编程特性的核心组成部分。

1. 闭包基础

1.1 定义闭包

闭包使用 || 语法定义,参数放在竖线之间,函数体跟在后面:

1
2
3
4
5
6
7
fn main() {
// 定义一个闭包:接受参数 x,返回 x + 1
let add_one = |x| x + 1;

// 调用闭包,就像调用函数一样
println!("结果: {}", add_one(5)); // 输出: 结果: 6
}

1.2 闭包的语法形式

闭包有多种等价的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fn main() {
// 完整的类型标注(最详细,但通常不需要)
let add_one_v1 = |x: u32| -> u32 { x + 1 };

// 省略返回类型(编译器可以推断)
let add_one_v2 = |x: u32| { x + 1 };

// 省略参数类型(编译器可以推断)
let add_one_v3 = |x| { x + 1 };

// 单行表达式可省略花括号(最简洁)
let add_one_v4 = |x| x + 1;

println!("{}", add_one_v4(5)); // 输出: 6
}

注意:一旦闭包的类型被推断确定,就不能再用不同类型调用它。

1.3 闭包与函数的对比

1
2
fn  add_one_fn   (x: u32) -> u32 { x + 1 }  // 普通函数定义
let add_one_cl = |x: u32| -> u32 { x + 1 }; // 闭包定义

主要区别

  • 闭包使用 || 而不是 ()
  • 闭包的类型标注通常是可选的(可以被推断)
  • 最重要的区别:闭包可以捕获环境变量,函数不能

2. 捕获环境

2.1 不可变借用

闭包默认以最小权限捕获变量:

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
let list = vec![1, 2, 3];
println!("定义闭包前: {:?}", list);

// 这个闭包只是读取 list,所以使用不可变借用
// 闭包"捕获"了外部的 list 变量
let only_borrows = || println!("闭包中: {:?}", list);

println!("调用闭包前: {:?}", list); // list 仍然可用
only_borrows(); // 调用闭包
println!("调用闭包后: {:?}", list); // list 仍然可用
}

2.2 可变借用

如果闭包需要修改变量,会自动使用可变借用:

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let mut list = vec![1, 2, 3];
println!("调用前: {:?}", list);

// 这个闭包修改了 list,所以使用可变借用
// 注意:闭包本身也要声明为 mut
let mut borrows_mutably = || list.push(7);

borrows_mutably(); // 调用闭包,修改 list
println!("调用后: {:?}", list); // 输出: [1, 2, 3, 7]
}

2.3 获取所有权

在某些情况下,闭包会获取变量的所有权:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use std::thread;

fn main() {
let list = vec![1, 2, 3];
println!("主线程: {:?}", list);

// move 关键字强制闭包获取 list 的所有权
// 这在多线程中很常见,因为新线程需要拥有数据
thread::spawn(move || println!("线程中: {:?}", list))
.join()
.unwrap();

// println!("{:?}", list); // 错误!list 已被移动到闭包中
}

注意:使用 move 关键字强制闭包获取所有权,这在多线程编程中很常见。

3. 闭包的三种 Trait

Rust 的闭包会自动实现以下一个或多个 trait,这决定了闭包如何使用捕获的变量:

3.1 Fn - 不可变借用

1
2
3
4
5
6
7
8
9
10
11
12
13
// 这个函数接受一个实现了 Fn trait 的闭包
// Fn(i32) -> i32 表示:接受 i32 参数,返回 i32
fn apply_fn<F>(f: F, x: i32) -> i32
where
F: Fn(i32) -> i32, // F 必须实现 Fn trait
{
f(x) // 调用闭包
}

fn main() {
let double = |x| x * 2; // 这个闭包实现了 Fn trait
println!("结果: {}", apply_fn(double, 5)); // 输出: 10
}

特点

  • 闭包只是读取捕获的变量(不可变借用)
  • 可以被多次调用
  • 最灵活的闭包类型

3.2 FnMut - 可变借用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 这个函数接受一个实现了 FnMut trait 的闭包
fn apply_fn_mut<F>(mut f: F, x: i32) -> i32
where
F: FnMut(i32) -> i32, // F 必须实现 FnMut trait
{
f(x)
}

fn main() {
let mut total = 0;

// 这个闭包修改了 total,所以实现了 FnMut trait
let mut accumulate = |x| {
total += x; // 修改外部变量
total
};

println!("累加后: {}", accumulate(5)); // 输出: 5
println!("累加后: {}", accumulate(3)); // 输出: 8
}

特点

  • 闭包修改了捕获的变量(可变借用)
  • 可以被多次调用
  • 需要声明为 mut

3.3 FnOnce - 消耗捕获的变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 这个函数接受一个实现了 FnOnce trait 的闭包
fn consume_with_closure<F>(func: F)
where
F: FnOnce(), // F 必须实现 FnOnce trait
{
func(); // 只能调用一次
}

fn main() {
let s = String::from("hello");

// 这个闭包会消耗(drop)s,所以只实现了 FnOnce trait
let consume = || drop(s); // drop 会获取 s 的所有权

consume_with_closure(consume);
// consume_with_closure(consume); // 错误!闭包已被消费
// println!("{}", s); // 错误!s 已被移动
}

特点

  • 闭包获取并消耗了捕获变量的所有权
  • 只能被调用一次
  • 最受限的闭包类型

4. 实际应用示例

4.1 结合迭代器使用

闭包最常见的用法是和迭代器一起使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn main() {
let numbers = vec![1, 2, 3, 4, 5];

// map: 使用闭包将每个元素加倍
let doubled: Vec<i32> = numbers.iter()
.map(|x| x * 2) // 闭包:每个元素乘以 2
.collect();

println!("翻倍后: {:?}", doubled); // [2, 4, 6, 8, 10]

// filter: 使用闭包过滤出偶数
let evens: Vec<&i32> = numbers.iter()
.filter(|&x| x % 2 == 0) // 闭包:判断是否为偶数
.collect();

println!("偶数: {:?}", evens); // [2, 4]
}

4.2 结合 sort_by_key 使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#[derive(Debug)]
struct User {
name: String,
age: u32,
}

fn main() {
let mut users = vec![
User { name: String::from("Alice"), age: 30 },
User { name: String::from("Bob"), age: 25 },
User { name: String::from("Charlie"), age: 35 },
];

// sort_by_key 接受一个闭包,用于提取排序的键
users.sort_by_key(|u| u.age); // 闭包:提取 age 字段进行排序

println!("按年龄排序: {:#?}", users);
}

4.3 延迟计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 定义一个缓存结构,只在第一次调用时计算
struct Cacher<T>
where
T: Fn(u32) -> u32, // T 是一个闭包类型
{
calculation: T, // 存储闭包
value: Option<u32>, // 存储计算结果
}

impl<T> Cacher<T>
where
T: Fn(u32) -> u32,
{
fn new(calculation: T) -> Cacher<T> {
Cacher {
calculation,
value: None, // 初始时没有值
}
}

fn value(&mut self, arg: u32) -> u32 {
match self.value {
Some(v) => v, // 如果已经计算过,直接返回
None => {
// 第一次调用,执行闭包计算
let v = (self.calculation)(arg);
self.value = Some(v); // 缓存结果
v
}
}
}
}

fn main() {
// 创建一个"昂贵"的计算闭包
let mut expensive = Cacher::new(|num| {
println!("慢速计算中...");
std::thread::sleep(std::time::Duration::from_secs(2));
num
});

println!("结果: {}", expensive.value(10)); // 第一次:会执行计算
println!("结果: {}", expensive.value(10)); // 第二次:直接返回缓存的结果
}

5. 闭包与所有权

5.1 Copy 类型的捕获

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
let x = 5; // i32 实现了 Copy trait

// move 关键字:闭包获取 x 的所有权
// 但因为 i32 是 Copy 类型,实际上是复制了一份
let equal_to_x = move |z| z == x;

println!("x 仍可用: {}", x); // 5,x 仍然有效(因为被复制了)

let y = 5;
assert!(equal_to_x(y)); // 闭包使用的是 x 的副本
}

5.2 非 Copy 类型的捕获

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let s = String::from("hello"); // String 没有实现 Copy

// 闭包通过不可变借用捕获 s
let print_s = || println!("{}", s);

print_s(); // 第一次调用
print_s(); // 第二次调用,因为只是借用,可以多次调用

println!("{}", s); // s 仍然有效
}

5.3 move 导致的所有权转移

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let s = String::from("hello"); // String 没有实现 Copy

// move 关键字:强制闭包获取 s 的所有权
let consume = move || {
let _s = s; // s 被移动到闭包内部
};

consume(); // 调用闭包,s 被消耗
// println!("{}", s); // 错误!s 的所有权已经转移给闭包
}

6. 函数指针与闭包

函数指针可以作为不捕获环境的闭包使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 普通函数
fn add_one(x: i32) -> i32 {
x + 1
}

// 接受函数指针作为参数
// fn(i32) -> i32 是函数指针类型
fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
f(arg) + f(arg) // 调用函数两次
}

fn main() {
// 传递函数指针
let answer = do_twice(add_one, 5);
println!("结果: {}", answer); // 12 (6 + 6)

// 也可以传递闭包(如果闭包不捕获环境)
let answer2 = do_twice(|x| x + 1, 5);
println!("结果: {}", answer2); // 12
}

7. 返回闭包

由于闭包的大小在编译时未知,返回闭包需要使用 trait 对象:

1
2
3
4
5
6
7
8
9
10
// 使用 Box 包装闭包,因为闭包的大小未知
// dyn Fn(i32) -> i32 表示"任何实现了 Fn(i32) -> i32 的类型"
fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
Box::new(|x| x + 1) // 将闭包装箱返回
}

fn main() {
let f = returns_closure(); // f 的类型是 Box<dyn Fn(i32) -> i32>
println!("结果: {}", f(5)); // 6
}

或使用 impl Trait 语法(更简洁,但需要单一具体类型):

1
2
3
4
5
6
7
8
9
10
// impl Fn(i32) -> i32 表示"返回某个实现了 Fn(i32) -> i32 的类型"
// 编译器会自动推断具体类型
fn returns_closure() -> impl Fn(i32) -> i32 {
|x| x + 1
}

fn main() {
let f = returns_closure();
println!("结果: {}", f(5)); // 6
}

8. 常见应用场景

8.1 错误处理

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let numbers = vec!["1", "2", "three", "4"];

// filter_map: 结合过滤和映射
// parse().ok() 将 Result 转换为 Option,失败时返回 None
let parsed: Vec<i32> = numbers.iter()
.filter_map(|s| s.parse().ok()) // 闭包:尝试解析,只保留成功的
.collect();

println!("解析成功: {:?}", parsed); // [1, 2, 4]("three"被过滤掉了)
}

8.2 自定义迭代器适配器

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let numbers = vec![1, 2, 3, 4, 5];

// 链式调用多个闭包
let sum: i32 = numbers.iter()
.map(|x| x * x) // 闭包1:计算平方
.filter(|x| x % 2 == 0) // 闭包2:过滤偶数
.sum(); // 求和

println!("偶数平方和: {}", sum); // 4 + 16 = 20
}

8.3 回调函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 接受一个闭包作为回调函数
// F: Fn(i32) 表示 F 是一个接受 i32 参数的闭包
fn process_data<F>(data: Vec<i32>, callback: F)
where
F: Fn(i32), // 回调函数类型
{
for item in data {
callback(item); // 对每个元素调用回调
}
}

fn main() {
let data = vec![1, 2, 3, 4, 5];

// 传递一个闭包作为回调
process_data(data, |x| {
println!("处理: {}", x);
});
}

总结

闭包是 Rust 中强大而灵活的特性,它能够:

  • 简洁表达:用简短的语法定义匿名函数
  • 捕获环境:访问定义时作用域内的变量
  • 类型推断:编译器自动推断参数和返回值类型
  • 零成本抽象:编译后性能与手写代码相当
  • 灵活的所有权:根据需要选择借用或移动语义

三种 Trait 总结

  • Fn:只读取捕获的变量,可以多次调用
  • FnMut:可以修改捕获的变量,可以多次调用
  • FnOnce:会消耗捕获的变量,只能调用一次

掌握闭包是编写惯用的、高性能 Rust 代码的关键。它们与迭代器结合使用,能够写出既简洁又高效的代码,是 Rust 函数式编程风格的核心。

Hooray!Closures 小节完成!!!