迭代器(Iterators)
本教程共 78 篇 · 第 49 篇 · 更新于 2026-08-08 · 约 11 分钟阅读
本节目标:理解迭代器如何遍历集合、为什么是惰性的、next 方法怎么工作,并分清 into_iter/iter/iter_mut 与”消费者/适配器”的分工。
资深 Rust 开发者常会说:写 Rust 最该掌握的之一就是迭代器。它让你只关心”每个元素怎么处理”,不用管从哪开始、到哪结束、按什么索引访问。
1-1 for 循环其实就是迭代器
Rust 里的 for 循环,本质是对迭代器的语法糖:
let arr = [1, 2, 3];
for v in arr {
println!("{v}");
}
它不像别的语言用索引 arr[i] 去访问,而是把 arr 当迭代器,直接逐个取出元素。数组本身不是迭代器,但它实现了 IntoIterator trait,for 糖自动把它转成迭代器。
也可以显式转换:
let arr = [1, 2, 3];
for v in arr.into_iter() {
println!("{v}");
}
范围也能直接迭代:for i in 1..10 就是迭代 1 到 9。
1-2 惰性初始化
迭代器是惰性的:光创建它,啥也不会发生,只有真正用起来才开始遍历。
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter(); // 此刻没有任何遍历发生
for val in v1_iter {
println!("{val}");
}
在 for 之前,只是造了个迭代器,零开销、不消耗元素。只有 for 启动,迭代才真正进行。这种惰性保证了”创建迭代器”这个动作本身几乎不花代价。
1-3 next 方法:迭代器的心脏
迭代器之所以是迭代器,是因为它实现了 Iterator trait,而这个 trait 最核心的方法就是 next:
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
next 每次取出下一个元素,包装在 Option 里:有值返回 Some(元素),没了返回 None。for 循环其实就是不停地调 next。
你也可以自己调:
fn main() {
let arr = [1, 2, 3];
let mut arr_iter = arr.into_iter();
assert_eq!(arr_iter.next(), Some(1));
assert_eq!(arr_iter.next(), Some(2));
assert_eq!(arr_iter.next(), Some(3));
assert_eq!(arr_iter.next(), None);
}
注意三点:
next返回Option,有值Some(T)、无值None;- 元素是按顺序依次取出的;
- 手动调
next必须把迭代器声明成mut,因为调用会改动内部状态(当前位置)。for循环不用标mut,它内部帮你处理了。
Note
next对元素是消耗性的:每调一次吃掉一个,最后迭代器空了,只能返回None。所以同一个迭代器不能无限制重复遍历。
1-4 into_iter、iter、iter_mut 三兄弟
把集合变迭代器,有三种方式,区别在于”怎么借”:
into_iter():拿走集合的所有权,遍历后原集合没了;iter():不可变借用,遍历的是引用&T;iter_mut():可变借用,拿到&mut T,能改元素。
fn main() {
let values = vec![1, 2, 3];
for v in values.into_iter() {
println!("{v}");
}
// 这里再用 values 会报错,所有权已被转移
let values = vec![1, 2, 3];
let _it = values.iter();
println!("{:?}", values); // 借用,原集合照常用
let mut values = vec![1, 2, 3];
let mut it_mut = values.iter_mut();
if let Some(v) = it_mut.next() {
*v = 0; // v 是 &mut i32,解引用后修改
}
println!("{:?}", values); // [0, 2, 3]
}
记忆窍门:into_ 开头多半是拿走所有权,_mut 结尾多半是可变借用,剩下就是不可变借用。
Tip
Iterator和IntoIterator别搞混:IntoIterator表示”我能变成一个迭代器”(靠into_iter);Iterator表示”我就是迭代器,能调next”。迭代器自身也实现了IntoIterator,所以链式.into_iter().into_iter()不会报错。
1-5 消费者适配器:吃掉迭代器,给个值
迭代器上的方法分两类。第一类叫消费者适配器,它会消耗迭代器,返回一个具体的值——因为它们内部都靠调 next 来取元素。最常见的就是 sum:
fn main() {
let v1 = vec![1, 2, 3];
let total: i32 = v1.iter().sum();
assert_eq!(total, 6);
println!("{:?}", v1); // 借用迭代器,原 v1 仍可用
}
sum 拿了迭代器的所有权,遍历求和后返回总数。调用后迭代器就没了,不能再 for 它。
1-6 迭代器适配器:返回新迭代器
第二类叫迭代器适配器,它不消耗,而是返回一个新的迭代器。这是链式调用的关键:v.iter().map(...).filter(...)。
但适配器是惰性的——光写不消费,啥也不干:
let v1 = vec![1, 2, 3];
v1.iter().map(|x| x + 1); // 警告:什么都没发生!
必须用消费者适配器收尾。比如 collect 把结果收成集合:
let v1 = vec![1, 2, 3];
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
assert_eq!(v2, vec![2, 3, 4]);
map(|x| x + 1) 把每个元素加 1,但只是”规划”了一下;collect 一收尾,规划才真正执行,最终得到新向量。collect 很强大,能收成 Vec、HashMap 等多种集合,所以要标类型告诉它收成啥。
filter 也是适配器,按闭包条件保留元素:
let v = vec![1, 2, 3, 4];
let evens: Vec<_> = v.into_iter().filter(|x| x % 2 == 0).collect();
assert_eq!(evens, vec![2, 4]);
1-7 适配器链为什么是零成本
你可能会担心:map、filter 一层层包起来,会不会产生很多临时对象和函数调用开销?答案是不会。迭代器适配器是”惰性”的——map 返回的只是一个新的迭代器结构体,它内部记下”下一步要套用这个闭包”,并不会立刻跑任何元素。
只有当 collect、sum、for 这类消费者出现,整条链才真正启动:next() 每被调用一次,前面各个适配器就地依次作用在同一元素上。再加上单态化,编译器为这套具体类型生成了专用代码,并能把闭包内联、把循环优化到几乎和手写 for 一样。所以”迭代器写法”和”手写循环”在性能上持平,却往往更易读。
Note因为适配器不分配堆内存、不存中间集合,一条长链的内存占用非常小。这正是 Rust 鼓励用迭代器改写循环的底气:表达力强,还不付运行时代价。
1-8 常用适配器与消费者一览
迭代器的方法多到记不住,但常用的就那几个,按”做什么”分类记最快。适配器(返回新迭代器、惰性):map 变换每个元素、filter 按条件留元素、take(n) / skip(n) 截取前/后、chain 拼接两条、rev 反转、zip 配对、enumerate 带下标。
消费者(吃掉迭代器、给出结果):collect 收成集合、sum / product 求和求积、count 计数、min / max 最值、fold 自定义累积、any / all 判断、find 找第一个、last 取末尾。
let v = [1, 2, 3, 4, 5];
let s: i32 = v.iter().filter(|&&x| x % 2 == 1).map(|x| x * x).sum();
// 奇数 1,3,5 的平方和 = 1+9+25 = 35
Tip记不住没关系,写的时候按”我要变换、要筛选、要配对、最后要个结果”去翻这几个名字就行。它们是积木,拼出你想要的流水线。
1-9 再看清 for 循环与迭代器的关系
很多人以为 for 循环和迭代器是两套东西,其实 for 循环的底层就是迭代器。写 for x in v 时,编译器会自动把 v 变成 v.into_iter(),然后不断调用 next(),直到返回 None。也就是说:
let v = vec![1, 2, 3];
// 下面两种完全等价
for x in &v { println!("{x}"); }
let mut it = v.iter();
while let Some(x) = it.next() { println!("{x}"); }
明白这点后,“迭代器不是什么魔法”就落地了:它只是把”取下一个元素”这件事做成了可组合、可惰性、可单态化的接口。你用 map/filter 不过是提前把”每一步怎么处理”拼好,最后由 for 或 collect 这样的消费者去真正驱动 next()。
Tip以后再看到迭代器链式写法,脑子里把它还原成”一个带预处理步骤的 for 循环”,理解成本和心智负担都会降下来。
1-10 小结
for循环是迭代器的语法糖;迭代器由实现了IntoIterator的集合转换而来;- 迭代器是惰性的:创建不做事,使用才遍历;
next是核心,返回Option,消耗性地逐个取元素;into_iter拿所有权、iter借用、iter_mut可变借用;- 消费者适配器(如
sum)吃掉迭代器给个值;迭代器适配器(如map/filter)返回新迭代器,需消费者收尾; collect把迭代结果收集成集合,要标目标类型。
下一章我们把平时写的 for 循环,改写成迭代器风格,体会两种写法的差别。