闭包和迭代器在 Rust 里随处可见。等你熟悉了这两样,再去读 Rust 项目和库的代码,到处都能碰到它们。
我刚学 Rust 的时候,觉得这两样东西的语法有点吓人,拖了很久才去学。现在想想,早该试试了。它们其实是我在 Rust 里用得最顺手的特性之一。
我一直拖着没学,主要是没搞懂闭包为什么要用到 move 这个关键字。
OK,我们一上来就来个比较难的。
1. 什么是闭包
如果你写过 JavaScript,可能已经用过闭包了。
闭包(closure)可以像函数一样调用。你可以直接在需要它的地方定义它,它能捕获周围作用域里的变量(万恶之源)。
Python 有 lambda,Java 8 引入了 lambda 表达式,C++ 也有 lambda 函数。Rust 的闭包跟它们很像,还能和所有权机制配合。
fn main() {
let add = |x, y| x + y;
println!("{}", add(3, 4)); // 7
}
|x, y| 里的 x 和 y 是参数。这里不用标类型,Rust 会根据用法推断。闭包返回的是 x + y 的结果。
闭包体里要写多条语句时,用花括号括起来。
fn main() {
let describe = |n: i32| {
if n > 0 {
String::from("positive")
} else {
String::from("non-positive")
}
};
println!("{}", describe(5));
}
2. 闭包可以捕获外部变量
普通函数不能直接访问定义处的局部变量,闭包却可以。比如下面的 is_big 就用到了外面的 threshold。
fn main() {
let threshold = 10;
let is_big = |n| n > threshold; // captures `threshold`
println!("{}", is_big(15)); // true
println!("{}", is_big(5)); // false
}
Python 的 lambda 也能这么做。在 C++ 里,可以用 [threshold] 按值捕获,或者用 [&threshold] 按引用捕获。Rust 会看你在闭包里怎么用 threshold,再推断捕获方式。
3. 闭包的三个 trait
我第一次碰到闭包相关的编译错误时,完全不知道 Fn、FnMut 和 FnOnce 是什么意思。这三个 trait 的区别,可以从怎么调用闭包看出来。
FnOnce的调用会消耗闭包。所有闭包都实现它,但如果你只知道一个闭包满足FnOnce,就只能调用它一次。把捕获的值从闭包里移出去,就是只能调用一次的典型情况。FnMut可以反复调用,调用时需要可变借用闭包。闭包可以修改捕获的状态。Fn也可以反复调用,只需共享借用闭包。闭包体不会移出捕获的值,也无需可变借用捕获的状态。
Rust 根据闭包体的行为自动实现这些 trait。Fn 闭包也实现 FnMut 和 FnOnce,FnMut 闭包也实现 FnOnce。
fn apply_once<F: FnOnce() -> String>(f: F) {
println!("{}", f()); // can only call f once
}
fn apply_twice<F: Fn() -> i32>(f: F) {
println!("{}", f());
println!("{}", f()); // can call f multiple times
}
fn main() {
let msg = String::from("hello");
apply_once(move || msg); // msg is moved into closure, consumed on call
let x = 5;
apply_twice(|| x * 2); // x is borrowed, can be used many times
}
平时很少需要显式考虑该用哪个 trait。类型不匹配时,编译器会指出来。
4. move 闭包
有时你希望闭包按值捕获变量。加上 move,闭包会把捕获的值移进去。如果值实现了 Copy,就复制一份。
fn main() {
let name = String::from("Zeeshan");
let greet = move || println!("Hello, {}!", name);
greet();
// println!("{}", name); // ERROR: name was moved into the closure
}
把闭包传给线程,或者把它存起来以后再用时,经常会用到 move。上面的 greet 拥有 name,调用时只借用它来打印,所以仍然可以反复调用。
5. 迭代器
迭代器每次产生一个值。实现了 Iterator trait 的类型可以放进循环,可以接上各种适配器,也可以把产生的值收集到数据结构里。
这个 trait 的核心是 next。
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
每次调用 next(),有值就返回 Some(value)。for 循环先通过 IntoIterator 得到迭代器,再反复调用 next(),遇到 None 就结束。有些迭代器在返回 None 后,再调用 next() 仍可能得到值。
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
for n in numbers.iter() {
println!("{}", n);
}
}
6. 迭代器适配器
适配器(adapter)用来转换迭代器产生的序列。它们是惰性的,等到你消费迭代器时才会处理元素。
这有点像 Python 的生成器表达式,也可以类比 Java 的 Stream。Rust 的迭代器链可以用静态分派,适配器本身也不要求装箱。实际速度还要看具体操作和编译优化。
map 对每个元素做一次转换。
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let doubled: Vec<i32> = numbers.iter().map(|&n| n * 2).collect();
println!("{:?}", doubled); // [2, 4, 6, 8, 10]
}
filter 保留满足条件的元素。
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6];
let evens: Vec<&i32> = numbers.iter().filter(|&&n| n % 2 == 0).collect();
println!("{:?}", evens); // [2, 4, 6]
}
fold 把序列归约成一个值,类似 Python 或 JavaScript 的 reduce。它会消费迭代器,直接返回结果。
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let sum = numbers.iter().fold(0, |acc, &n| acc + n);
println!("{}", sum); // 15
}
求和、求积这类常见的归约操作,还可以直接用 .sum() 和 .product()。
7. 把适配器串起来
我喜欢 Rust 迭代器的一点是,几个操作可以顺着写下来。比如筛出偶数、求平方,再把结果收集成一个 Vec。这条链只需遍历一次输入,中间也不用先建个列表来存偶数。
fn main() {
let data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let result: Vec<i32> = data
.iter()
.filter(|&&n| n % 2 == 0) // keep evens: 2,4,6,8,10
.map(|&n| n * n) // square them: 4,16,36,64,100
.collect();
println!("{:?}", result);
}
这就是大家所熟知的链式调用!
用 Python 写,可以用列表推导式。用 Java 写,也可以用 Stream 依次做 filter、map 和 collect。Rust 的写法跟 Java Stream 很像。开启优化后,编译器可以把这样的适配器链优化成一个循环。
8. 几个常用的迭代器方法
enumerate 给每个元素配上从 0 开始的索引。
fn main() {
let fruits = vec!["apple", "banana", "cherry"];
for (i, fruit) in fruits.iter().enumerate() {
println!("{}: {}", i, fruit);
}
}
zip 把两个迭代器的元素配成一对。
fn main() {
let names = vec!["Alice", "Bob", "Carol"];
let scores = vec![95, 87, 92];
let combined: Vec<_> = names.iter().zip(scores.iter()).collect();
for (name, score) in &combined {
println!("{} scored {}", name, score);
}
}
flat_map 先转换,再把结果展开一层。
fn main() {
let sentences = vec!["hello world", "foo bar baz"];
let words: Vec<&str> = sentences
.iter()
.flat_map(|s| s.split_whitespace())
.collect();
println!("{:?}", words); // ["hello", "world", "foo", "bar", "baz"]
}
any 检查有没有元素满足条件,all 检查是否所有元素都满足条件。
fn main() {
let numbers = vec![1, 3, 5, 7, 9];
println!("{}", numbers.iter().any(|&n| n > 5)); // true
println!("{}", numbers.iter().all(|&n| n % 2 != 0)); // true (all odd)
}
9. 自己实现 Iterator
想让自己的类型可以迭代,可以给它实现 Iterator。写过 C++ 自定义迭代器的读者应该会觉得熟悉,Rust 的写法更直接。
struct Counter {
count: u32,
max: u32,
}
impl Counter {
fn new(max: u32) -> Self {
Counter { count: 0, max }
}
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
if self.count < self.max {
self.count += 1;
Some(self.count)
} else {
None
}
}
}
fn main() {
let counter = Counter::new(5);
let sum: u32 = counter.sum();
println!("{}", sum); // 15
// or use it with all the other adapters
let doubled: Vec<u32> = Counter::new(5).map(|n| n * 2).collect();
println!("{:?}", doubled); // [2, 4, 6, 8, 10]
}
对上面的 Counter 来说,只要定义元素类型和取下一个值的方法,就能用 sum 求和,也能接上 map 和 collect。
10. 闭包和迭代器一起用
大多数迭代器适配器会接收闭包,所以这两个话题放在一起讲正合适。比如,要找出薪水超过 80,000 的员工姓名,再算出所有人的薪资总额,可以这么写。
fn main() {
let employees = vec![
("Alice", 85000),
("Bob", 72000),
("Carol", 91000),
("David", 68000),
];
let senior_names: Vec<&str> = employees
.iter()
.filter(|(_, salary)| *salary > 80000)
.map(|(name, _)| *name)
.collect();
println!("{:?}", senior_names); // ["Alice", "Carol"]
let total_payroll: i32 = employees.iter().map(|(_, s)| s).sum();
println!("Total payroll: {}", total_payroll);
}
在 Java 有 Stream 之前,这类操作通常要在循环里分几步做。Python 可以用列表推导式,C 里直接处理数组则容易写得又长又出错。Rust 的这段代码顺着方法名就能读懂,也能编译成高效的机器码。
小结
闭包可以就地定义,也能捕获周围作用域里的变量。FnOnce、FnMut 和 Fn 描述了调用闭包时需要怎样访问它。迭代器按需产生值,还提供了一组可以串起来的数据转换方法。两者配合,能把数据转换写得清楚、高效,也能保留 Rust 的安全保证。
熟悉这两样之后,很多地方就不用手写 for 循环了。我现在写的函数几乎都会用到它们。