跳转到内容

Rust 迭代器

迭代器(iterator)就是能一个接一个地产生一串值的东西。其实从 for 循环那一页开始,你就一直在用它,只是还没叫出它的名字:每个遍历集合的 for 循环,背后都是迭代器在干活。

fn main() {
let numbers = vec![1, 2, 3];
let mut iter = numbers.iter();
println!("{:?}", iter.next()); // Some(1)
println!("{:?}", iter.next()); // Some(2)
println!("{:?}", iter.next()); // Some(3)
println!("{:?}", iter.next()); // None
}

输出:

Some(1)
Some(2)
Some(3)
None
▶ 动手试一试

.next() 返回一个 Option:还有元素时返回 Some(值),元素用完了就返回 None。for 循环说穿了,就是在背后不停地调用 .next(),直到拿到 None 为止。

.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]
▶ 动手试一试

.collect() 负责把迭代器重新变回一个具体的集合(这里是 Vec<i32>)。少了它,你手里就只有一个还没开始计算的迭代器,什么也不会发生。

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]
▶ 动手试一试

两个星号的 **n 看起来有点怪。这是因为 .filter() 交给闭包的是“引用的引用”(&&i32),要和普通数字比较,就得解引用两次。这是个常见的小别扭,现在不用急着弄懂背后的全部原因。

链式调用迭代器方法的好处,从这里开始显现:几个步骤可以合成一条清晰易读的流水线。

fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6];
let result: Vec<i32> = numbers
.iter()
.filter(|n| **n % 2 == 0) // 只保留偶数
.map(|n| n * 10) // 每个都乘以 10
.collect();
println!("{:?}", result);
}

输出:

[20, 40, 60]
▶ 动手试一试

可以和手写的版本比一比:用 for 循环,再往一个空 Vec 里 push。那样写也没错,但链式写法读起来就是“先筛选,再乘以 10,最后收集”,和你脑子里想的步骤一模一样。

fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let total: i32 = numbers.iter().sum();
let count = numbers.iter().count();
let has_even = numbers.iter().any(|n| n % 2 == 0);
println!("总和:{total},个数:{count},有偶数:{has_even}");
}

输出:

总和:15,个数:5,有偶数:true
▶ 动手试一试

.sum() 把所有元素加起来,.count() 告诉你一共有几个元素,.any() 回答“是不是至少有一个元素满足条件”。这些只是每个迭代器都有的方法里的一小部分,还有很多,比如 enumerate、zip、take 和 rev。在自定义迭代器那一页,你会亲手写一个迭代器,并看到它白白就拥有了所有这些方法。

取 1 到 10 这几个数,只保留奇数,把每个数平方,再全部加起来。要求用一条迭代器链完成,不用任何 mut 变量。提示:把 .filter()、.map() 和 .sum() 串起来,并给结果标上类型,比如 let total: i32。

查看答案
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let total: i32 = numbers
.iter()
.filter(|n| **n % 2 == 1) // 保留 1, 3, 5, 7, 9
.map(|n| n * n) // 1, 9, 25, 49, 81
.sum();
println!("奇数的平方和:{total}");
}

输出:

奇数的平方和:165
▶ 动手试一试

total 上必须写 : i32,因为 .sum() 可以加总成好几种数字类型,Rust 猜不出你想要哪一种。

从 vec!["the", "quick", "brown", "fox", "jumps"] 开始,构建一个新的 Vec<String>,里面只放长度超过 3 个字母的单词,并且全部转成大写,然后用 {:?} 打印出来。(这道题要按长度筛选、再转大写,所以单词保留英文:一个汉字在 UTF-8 里占 3 个字节,也没有大小写之分。)提示:.filter(|w| w.len() > 3) 留下长单词,.to_uppercase() 把 &str 转成大写的 String。

查看答案
fn main() {
let words = vec!["the", "quick", "brown", "fox", "jumps"];
let loud: Vec<String> = words
.iter()
.filter(|w| w.len() > 3)
.map(|w| w.to_uppercase())
.collect();
println!("{:?}", loud);
}

输出:

["QUICK", "BROWN", "JUMPS"]
▶ 动手试一试

words 本身没有任何变化。.iter() 只是借用它,所以这条链之后你照样可以使用 words。

  • 迭代器通过 .next() 一次产生一个值,.next() 返回的是 Option。
  • for 循环的背后就是迭代器。
  • .map() 转换每个元素;.filter() 只保留满足条件的元素;.collect() 把结果变回一个具体的集合。
  • 把迭代器方法串成链,通常比手写循环、再用一个可变变量累加结果更清楚。

小测验

1. 迭代器的 .next() 返回什么?

2. 在 .iter().map(...).collect() 这样的链里,最后的 .collect() 做了什么?

3. 在 .filter(|n| **n % 2 == 0) 里,为什么要对 n 解引用两次?

4. for 循环遍历集合时,背后靠的是什么?

5. .filter() 之后的迭代器里会留下哪些元素?

得分: 0 / 5