8.1 闭包与高阶函数
在 Rust 中,闭包(Closure)是一种可以捕获其定义环境中的变量的匿名函数。它们与普通函数类似,但具有更大的灵活性。高阶函数(Higher-Order Function)则是指那些接受闭包作为参数,或者将闭包作为返回值的函数。这一特性使得 Rust 能够以优雅、简洁的方式实现函数式编程风格。
8.1.1 什么是闭包?
闭包本质上是一个可以存储在变量中,或作为参数传递给其他函数的代码块。与函数不同,闭包可以捕获其定义作用域中的变量。
基本语法:
let add_one = |x: i32| -> i32 { x + 1 };
let result = add_one(5);
println!("{}", result); // 输出: 6
闭包的语法由一对竖线 || 包围的参数列表,后跟一个表达式或代码块组成。参数类型和返回类型通常可以省略,由编译器推断。
简化形式:
// 单行表达式,可以省略花括号
let add_two = |x| x + 2;
// 多行代码块,需要花括号
let complex_closure = |x: i32| {
let y = x * 2;
y + 3
};
8.1.2 捕获环境变量
闭包的一个核心特性是能够捕获其定义所在作用域中的变量。这通过三种方式实现,与函数的借用规则类似:
- 不可变借用 (
&T):闭包通过不可变引用来读取变量。 - 可变借用 (
&mut T):闭包通过可变引用来修改变量。 - 所有权转移 (
T):闭包取得变量的所有权。
编译器会根据闭包体内的操作自动推断捕获方式。
示例:不可变借用
let name = String::from("Rust");
let print_name = || println!("Hello, {}!", name); // 捕获 &name
print_name(); // 输出: Hello, Rust!
println!("{}", name); // 仍然可以访问 name
示例:可变借用
let mut count = 0;
let mut increment = || {
count += 1; // 捕获 &mut count
};
increment();
increment();
println!("{}", count); // 输出: 2
// 注意:在可变借用闭包最后一次使用之前,不能有其他对 count 的借用
示例:所有权转移
let data = vec![1, 2, 3];
let take_ownership = || {
let owned_data = data; // 捕获 data 的所有权
println!("{:?}", owned_data);
};
take_ownership();
// println!("{:?}", data); // 错误!data 的所有权已被转移
8.1.3 move 关键字
有时我们需要强制闭包取得捕获变量的所有权,即使它只是进行借用操作。这在使用多线程时尤为重要,因为线程可能比闭包捕获的变量活得更久。
move 关键字放在闭包参数列表之前,指示编译器将所有捕获的变量所有权转移到闭包中。
let data = vec![1, 2, 3];
let thread_closure = move || {
println!("Data from thread: {:?}", data);
};
std::thread::spawn(thread_closure).join().unwrap();
// 此时 data 的所有权已转移到闭包中,主线程无法再访问
8.1.4 闭包作为函数参数(高阶函数)
将闭包作为参数传递给函数是 Rust 中非常常见的模式。为了在函数签名中描述闭包的类型,我们使用 Fn、FnMut 和 FnOnce 这三个 trait。
FnOnce:表示闭包只能被调用一次。如果一个闭包捕获了变量的所有权,或者实现了Droptrait,它通常实现FnOnce。FnMut:表示闭包可以多次被调用,并且可能会修改捕获的变量。Fn:表示闭包可以多次被调用,并且不会修改捕获的变量(只进行不可变借用)。
示例:使用 Fn trait
fn apply_twice<F>(f: F, x: i32) -> i32
where
F: Fn(i32) -> i32,
{
f(f(x))
}
fn main() {
let double = |x| x * 2;
let result = apply_twice(double, 3);
println!("{}", result); // 输出: 12 (3 * 2 * 2)
}
示例:使用 FnMut trait
fn call_mut<F>(mut f: F, x: i32)
where
F: FnMut(i32) -> i32,
{
let result = f(x);
println!("{}", result);
}
fn main() {
let mut acc = 0;
let mut add_to_acc = |x| {
acc += x;
acc
};
call_mut(add_to_acc, 5); // 输出: 5
}
示例:使用 FnOnce trait
fn call_once<F>(f: F, x: i32)
where
F: FnOnce(i32) -> i32,
{
let result = f(x);
println!("{}", result);
}
fn main() {
let data = String::from("hello");
let consume = |x| {
println!("{}", data); // 捕获了 data 的所有权
x + 1
};
call_once(consume, 10); // 输出: hello \n 11
}
8.1.5 闭包作为返回值
函数也可以返回闭包。由于闭包类型在编译时是未知的,我们需要使用 impl Trait 语法来返回一个实现了特定 trait 的闭包。
fn create_adder(x: i32) -> impl Fn(i32) -> i32 {
move |y| x + y // 必须使用 move 关键字,因为 x 在函数返回后可能被销毁
}
fn main() {
let add_five = create_adder(5);
let result = add_five(10);
println!("{}", result); // 输出: 15
}
8.1.6 常见的高阶函数
Rust 的标准库提供了大量使用闭包的高阶函数,尤其是在迭代器(Iterator)上。
map:对每个元素应用一个闭包,返回一个新的迭代器。filter:根据闭包返回的布尔值过滤元素。fold/reduce:将闭包应用于所有元素,累积成一个单一值。for_each:对每个元素执行一个闭包,主要用于副作用操作。
示例:链式调用
let numbers = vec![1, 2, 3, 4, 5];
let sum_of_squares_of_evens: i32 = numbers
.iter()
.filter(|&&x| x % 2 == 0) // 过滤出偶数
.map(|&x| x * x) // 计算平方
.sum(); // 求和
println!("{}", sum_of_squares_of_evens); // 输出: 20 (4 + 16)
8.1.7 性能考量
闭包在 Rust 中是零开销抽象的。编译器会将闭包内联展开,生成与手写循环或显式函数调用几乎相同的机器码。因此,使用闭包不会带来运行时性能损失,同时还能提升代码的表达力和可读性。
总结
闭包和高阶函数是 Rust 语言中强大的工具。它们允许你以声明式的方式编写代码,将行为作为数据进行传递,从而构建出更灵活、更模块化的程序。理解 Fn、FnMut 和 FnOnce 这三个 trait 是掌握 Rust 闭包的关键,它们与 Rust 的所有权系统紧密结合,确保了内存安全。
