
作为一个常年写 Rust 的工程师我几乎每天都会跟Iterator打交道。说实话刚接触这个概念时我还挺不屑的——不就是个next()方法吗C 里也有迭代器Java 里也有有什么好研究的。但随着我踩的坑越来越多我才发现IteratorTrait 里的门道远比我想象的多next()只是冰山一角。这篇内容就是想把我这几年的实际体会整理出来从核心方法的结构逻辑、常见陷阱到性能表现一次性讲透。如果你是一个正在学 Rust 的初学者或者已经开始写一些实际项目但过程中对迭代器总是“能用但说不清”那这篇内容很适合你。我会把它拆成几个层面来讲先弄明白它到底是什么、为什么这样设计再讲怎么自己实现一个迭代器然后结合我踩过的坑、做过的性能测试分享一些真正有用的实践经验。1. 一个方法撑起整个抽象next() 与懒加载逻辑IteratorTrait 是标准库里最典型的“小而精”的抽象。官方定义里它只有一个必须实现的方法就是next()返回OptionSelf::Item。但千万别小看这个签名它背后藏着 Rust 迭代器设计的两个核心思想。1.1 为什么返回 Option 而不是直接返回元素这是 Rust 区别于 C 迭代器的一个关键点。C 的迭代器用operator和operator*组合来表达“移动”和“取值”但这种方式有两个问题一是很容易出现迭代器指向容器末尾之后的悬空位置二是不好表达“无限长的序列”。Rust 选择让迭代器自己暴露一个next()方法返回值是OptionT。如果迭代器还能继续产生元素就返回Some(item)如果已经到末尾了就返回None。这样做的直接好处是迭代器的边界条件被统一收敛到了 Option 里调用方不需要知道“这个迭代器什么时候算结束”只需要不断调next()直到拿到None。我最初写for循环的时候以为它只是一个语法糖等价于 C 语言里的for (int i 0; ...)。但其实for循环展开后是这样的let mut iter collection.into_iter(); while let Some(item) iter.next() { // 使用 item }这个模式的好处是它把“如何产生下一个元素”的逻辑完全封装进了next()调用方只关心“有没有下一个”。也就是说你不需要知道背后到底是一个数组、一个链表、一个文件、还是一个计算过程只要能实现next()就能统一地用for循环来遍历。1.2 懒加载迭代器不会主动执行理解了next()的核心地位你就明白为什么 Rust 的迭代器是惰性的了。所谓惰性指的是创建迭代器的动作本身不会做任何实际计算只有在你不断调用next()去“逼问”它的时候它才会逐个生成元素。举个例子let numbers vec![1, 2, 3, 4, 5]; let iter numbers.iter().map(|x| x * 2);上面这段代码运行时什么乘法都不会发生。map只是构造了一个新的Map结构体里面保存了对原迭代器的引用和一个闭包。只有当后面这个iter被消费比如collect()或for循环时map才会在每次next()调用中取出一个元素执行闭包里的x * 2再继续往下传。这个特点在很多场景下非常有用。最典型的就是无限序列。let fib (0..).scan((0u64, 1u64), |state, _| { let (a, b) *state; *state (b, a b); Some(a) });这段代码定义了一个永不停止的迭代器用来生成斐波那契数列。它不会因为“无限”而崩溃因为它根本不会提前把所有元素算出来。只有当你take(10)时它才实际计算前 10 个元素其余部分永远停留在“潜力”状态。我刚开始学 Rust 的时候不太习惯这种思维总感觉“写了map为什么没生效”。但其实这是迭代器体系的设计精髓我制造一个流水线但流水线只有在开始输送的时候才真正运转。理解了这一点后面很多别扭的代码就都想通了。1.3size_hint()给优化器的一个“提示”next()是唯一必须实现的方法但绝大多数实际使用的迭代器都会覆盖另一个方法size_hint()。它的签名是fn size_hint(self) - (usize, Optionusize) { (0, None) }返回的是一个元组第一个元素是下界第二个元素是上界如果是None就表示未知上限。为什么要有这个方法因为很多时候调用方想提前知道迭代器大约有几个元素以便一次性分配足够的内存而不是在collect过程中反复扩容。标准库里许多类型的size_hint都实现得很有意思比如Vec的迭代器下界和上界都精确等于长度而filter的迭代器下界往往是 0上界是原迭代器的长度因为经过过滤之后你永远无法准确预知到底会留下多少个。在日常代码里size_hint最常见的受益者是collect()内部的FromIterator实现。比如Vec::from_iter在收集元素时会先看迭代器的size_hint拿到下界n之后直接Vec::with_capacity(n)这就避免了 4 次扩容的反复内存分配。别小看这个优化在遍历几十万条数据做转换的场景里它能明显缩短总耗时。2. 消费与适配迭代器方法的两大阵营有了next()和惰性求值的基础你就能理解为什么IteratorTrait 提供了那么多方法了。标准库里Iterator的方法有几十个但我把它们分成两个阵营之后就清晰了很多适配器adapter和消费者consumer。2.1 适配器从迭代器到迭代器适配器方法的特点是它们接收一个迭代器返回一个新的迭代器。前面提到的map、filter都属于这一类还有take、skip、step_by、zip、chain、enumerate、inspect等。关键点是一个这些方法不会立刻消费迭代器只是往迭代链上追加了一层变换逻辑。let v vec![1, 2, 3, 4, 5, 6]; let doubled_even v.iter() .filter(|x| x % 2 0) // 保留偶数 .map(|x| x * 2); // 每个乘以2此时没有任何计算发生。doubled_even的类型是MapFilterIter_, i32, ..., ...一个嵌套很深的类型。手写这个类型非常痛苦所以大多数人后面会直接接collect()让编译器去推断类型。这里有一个经常被忽略的点适配器方法接收的是self还是mut self直接决定了它是否会消费原迭代器。比如map接收self所以它会“吃掉”原来的迭代器你无法在map之后再使用原来的迭代器变量。这个设计我认为很合理因为变换逻辑和原迭代器已经融合成了一个整体拆开反而会让状态不一致。2.2 消费者让迭代器真正跑起来消费者方法的特点是它们会不断调用next()直到耗尽迭代器然后返回一个聚合后的结果。collect、sum、count、fold、for_each、reduce、nth、last都属于这一类。我经常跟人讲如果一个函数签名里最终的返回值不是“某个迭代器”而是T、usize、OptionT、VecT这种具体值那它八成就是消费者。let total: i32 (1..100).sum(); let count (1..100).count(); let first_even (1..100).find(|x| x % 2 0);这些方法一旦执行迭代链上的所有惰性逻辑都会“瞬间启动”逐个元素地流经每个适配器最后汇总出结果。这一点特别像流水线的开工按钮前面搭建的各种传送带、加工站在这一刻全部运转起来。2.3 链式调用的返回值意义很多人刚接触迭代链时会困惑为什么我写v.iter().map(...)之后想要它的结果却发现类型不是Vec而是一个Map结构体因为map是适配器返回的是“尚未执行的变换”而不是结果集。所以当你想要最终结果时必须“消费”它let result: Veci32 v.iter().map(|x| x * 2).collect();collect之所以强大是因为它的返回值是泛型的可以收集到Vec、HashMap、HashSet、String等任何实现了FromIterator的容器类型。编译器根据你注解的目标类型自动选择合适的收集策略。如果你只想在迭代链上添加一个“只看不动”的观察点用inspectlet result: Vec_ v.iter() .inspect(|x| println!(before map: {x})) .map(|x| x * 2) .collect();inspect是适配器不会中断迭代链适合调试时打印中间状态。我调试迭代器链时经常用它比在闭包里加一堆打印要干净得多。3. 手写迭代器从简单到复杂的三层实践理解了方法体系接下来就应该动手写。自己实现迭代器是理解整个体系最快的路径。我按难度分了三层从标准到进阶都有对应场景。3.1 实现一个最基础的自定义迭代器假设我要实现一个简单的“奇数生成器”从某个起始值开始每次返回下一个奇数struct OddNumbers { current: i64, } impl OddNumbers { fn new(start: i64) - Self { // 如果 start 是偶数则取它下一个奇数 OddNumbers { current: if start % 2 0 { start 1 } else { start }, } } } impl Iterator for OddNumbers { type Item i64; fn next(mut self) - OptionSelf::Item { let result self.current; self.current 2; Some(result) } }这个迭代器是无限的每次next()都会返回Some。使用方式就是let odds: Veci64 OddNumbers::new(5).take(5).collect(); assert_eq!(odds, vec![5, 7, 9, 11, 13]);这个例子虽然简单但它展示了核心要点迭代器的本质是维护一个内部状态每次next()根据当前状态计算下一个元素并推进状态。我的current字段就是这个状态。真实世界的迭代器不外乎就是“状态 推进规则”的组合。3.2 实现双端迭代器next_back()与DoubleEndedIterator标准库里有一个DoubleEndedIteratorTrait它为那些可以从两个方向同时遍历的迭代器提供支持。核心方法是next_back()跟next()相对一个从前取一个从后取。以Vec的迭代器为例它同时支持next()和next_back()所以可以这样用let v vec![1, 2, 3, 4, 5]; let mut iter v.iter(); assert_eq!(iter.next(), Some(1)); assert_eq!(iter.next_back(), Some(5)); assert_eq!(iter.next(), Some(2)); assert_eq!(iter.next_back(), Some(4));这个能力有什么用最经典的是判断回文fn is_palindromeT: PartialEq(items: [T]) - bool { let mut iter items.iter(); while let (Some(left), Some(right)) (iter.next(), iter.next_back()) { if left ! right { return false; } } true }这个实现比“先反转再比较”要高效得多因为它只遍历了前半部分而不需要复制整个序列。自己实现一个双端迭代器时要注意必须确保next()和next_back()的边界互相正确。最典型的错误是两边都维护独立的索引结果前后两个方向走到交叉处时一个元素被返回了两次。以Vec为蓝本一个简化版的双端迭代器可以这样写struct BiItera, T { slice: a [T], } impla, T Iterator for BiItera, T { type Item a T; fn next(mut self) - OptionSelf::Item { let (first, rest) self.slice.split_first()?; self.slice rest; Some(first) } } impla, T DoubleEndedIterator for BiItera, T { fn next_back(mut self) - OptionSelf::Item { let (last, rest) self.slice.split_last()?; self.slice rest; Some(last) } }这样实现后两个方向共享同一个slice每取走一个元素slice就缩短一段永远不会重复取值。这是一个很实用的技巧用切片分割代替独立的前后索引天然规避了很多边界 bug。3.3 用状态机构造复杂迭代过程前面两个例子里的状态都挺简单但当迭代逻辑复杂时直接用若干字段表达状态很容易混乱。比如一个既能生成数据又能表示“规则变化”的迭代器或者一个内部有多个阶段需要切换的迭代器就不太适合只存一个current了。我推荐在迭代器内部使用枚举状态机。每个next()调用先匹配当前状态再决定如何推进。看一个例子实现一个迭代器依次产出“数组正序”、“数组元素乘以 2”、“数组逆序”三个阶段的元素。enum PhaseT { Forward(usize), Doubled(usize), Backward(usize), Done, } struct MultiPhaseIterT { data: VecT, phase: PhaseT, } implT: Clone Iterator for MultiPhaseIterT { type Item T; fn next(mut self) - OptionSelf::Item { match mut self.phase { Phase::Forward(i) { if *i self.data.len() { let item self.data[*i].clone(); *i 1; if *i self.data.len() { self.phase Phase::Doubled(0); } Some(item) } else { None } } Phase::Doubled(i) { if *i self.data.len() { let item self.data[*i].clone() * 2; *i 1; if *i self.data.len() { self.phase Phase::Backward(self.data.len()); } Some(item) } else { None } } Phase::Backward(i) { if *i 0 { *i - 1; Some(self.data[*i].clone()) } else { self.phase Phase::Done; None } } Phase::Done None, } } }这种状态机写法的好处非常明显每个阶段是独立的分支逻辑清晰后期要加一个“元素平方”的阶段只需要增加一个Phase变体和一个匹配分支。如果你用多个布尔变量和整数索引去表达这种多阶段切换很容易写着写着就自我矛盾。我自己的经验是迭代器内部状态一旦超过两个维度比如索引、方向、阶段就老老实实用枚举状态机。4. 实战中的雷区生命周期、借用与所有权问题迭代器不是单独存在的它总是和某种数据源绑定。数据源的所有权和借用关系决定了迭代器的合法使用范围。这一块是我见过新手报错最密集的区域。4.1iter()、iter_mut()与into_iter()的三者区别这是最基础但最容易混的问题。总结起来就三句话iter()产生T的迭代器即只读借用。iter_mut()产生mut T的迭代器即可变借用。into_iter()产生T的迭代器即拿走所有权。let v vec![1, 2, 3]; for x in v.iter() { // x: i32v 仍然可用 } for x in v.iter_mut() { // x: mut i32v 仍然可用但同一时间只能有一个可变借用 } for x in v.into_iter() { // x: i32v 已经被移动后面不能再用 }如果你在一个函数里先for x in v遍历了一遍后面还想用v那没问题但如果用了v.into_iter()编译器会直接拒绝你后续使用v。这一点在写代码时非常容易忽略尤其是重构的时候把iter()改成into_iter()之后后面的代码会突然连着一片报错。4.2 迭代器借用集合经典的“无法借用*self作为可变引用”有一种高频报错长这样“cannot borrow*selfas mutable more than once at a time”或者“cannot move out ofself.datawhich is behind a shared reference”。这类问题通常出在结构体里的方法返回迭代器的场景。举个例子我想给一个结构体写个方法返回一个迭代器让调用方可以遍历内部数据struct Store { data: Veci32, } impl Store { fn odd_iter(self) - impl IteratorItem i32 { self.data.iter().filter(|x| x % 2 1).copied() } }这段代码其实没问题因为filter借用的是self.data上的迭代器迭代器持有的是i32最后我用copied()把引用拷贝成i32返回不再依赖 self 的存活期。但如果你把返回类型写成impl IteratorItem i32其实也没问题只要生命周期标注正确impl Store { fn odd_iter(self) - impl IteratorItem i32 { self.data.iter().filter(|x| x % 2 1) } }这两种写法都能编译通过区别在于返回的是拷贝值还是引用。真正容易翻车的是试图返回一个修改内部数据的迭代器。比如impl Store { fn odd_iter_mut(mut self) - impl IteratorItem mut i32 { self.data.iter_mut().filter(|x| **x % 2 1) } }大多数情况下这个也能编译过但如果你在迭代器链里还试图调用self的其他方法问题就会出现。我的经验是不要在返回迭代器的方法里同时试图借用self的其他字段。迭代器已经借用了某个字段你再访问self的其他部分借用检查器会认为你同时持有了self的可变和共享引用。4.3 在循环中修改集合迭代器失效问题初学者最喜欢写的错误代码长这样let mut v vec![1, 2, 3, 4, 5]; for x in v { if *x % 2 0 { v.push(*x 1); // 编译错误无法在共享借用期间修改 v } }Rust 的借用检查器在这里直接阻止了你。这其实是好事——因为如果在迭代过程中修改Vecv背后的指针随时可能因为扩容而失效这在 C 里是典型的未定义行为。解决方式有几种传统方案先收集后修改let to_add: Veci32 v.iter().filter(|x| x % 2 0).map(|x| x 1).collect(); v.extend(to_add);用索引循环如果确实需要原地修改let mut i 0; while i v.len() { if v[i] % 2 0 { v.push(v[i] 1); } i 1; }这里用while循环而不是for是为了避免for基于Range迭代器时在v.len()变化后产生思维混乱。重点是你自己控制索引和长度Rust 不会在这里拦你因为你没有同时持有不可变借用。4.4collect时借用逃逸生命周期不匹配再来看一个常见的生命周期报错。假设有一个函数接收切片返回一个包含引用的Vecfn find_positionsa(s: a str, pattern: char) - Veca str { s.split(pattern).collect() }这个没问题因为split产生的子串切片和原字符串生命周期一致。但如果你试图在一个函数内部创建局部变量然后返回对它的迭代结果引用fn bad() - Vecstatic str { let local String::from(hello); local.split(l).collect() }编译器会拒绝因为local在函数结束时被释放返回的引用可能悬空。遇到这种问题要么返回拥有所有权的类型比如VecString要么把数据源的生命周期显式提升为入参。我自己写代码时有一条经验迭代器如果返回的是引用一定在函数签名里把生命周期标注清楚如果返回值不需要引用就尽量用copied()、cloned()或collect::Vec_()把数据转成拥有所有权的类型省得生命周期标注写错了编译器一头雾水。5. 性能探底iterator 真的零开销吗Rust 社区一直宣传“零成本抽象”。迭代器在优化开启后确实很多情况下不会比手写循环慢。但这个说法有前提。我做过几个小实验结论比宣传语更具体。5.1 循环融合map filter sum 的编译结果写一个典型例子let total: i64 data.iter() .filter(|x| x % 2 0) .map(|x| x * 3) .sum();手写等价的循环可能是let mut total 0; for x in data { if x % 2 0 { total x * 3; } }在rustc -O开启优化后这两段生成的机器码可以非常接近甚至完全一样。原因在于 LLVM 能把这些迭代器链路的每个next()调用都内联然后做循环融合loop fusion——也就是把filter的检查、map的乘法和sum的累加合并到一个循环体里。但要注意不是所有迭代器链都能融合。collect()会打断融合因为它通常需要先构建一个中间容器。如果你想做的是“过滤后求和”优先用sum()、fold()这类消费者而不是先collect再sum后者会产生一次不必要的内存分配和拷贝。以上结论基于常规优化编译结果具体到不同编译器版本、不同 CPU 会有差异但方向是明确的。5.2size_hint对collect的预分配影响前面提到collect()会利用size_hint做预分配。我们做一个简单实验let data: Veci32 (0..1_000_000).collect();Rangei32的size_hint是精确的所以Vec::from_iter会直接with_capacity(1_000_000)全程几乎零扩容。但如果是一个过滤后的迭代器let data: Veci32 (0..1_000_000).filter(|x| x % 2 0).collect();filter的size_hint下界是 0上界是 1_000_000。Vec会按上界预分配 1_000_000 的容量但实际只装了 500_000 个元素。这其实也没问题只是略微浪费了一点内存之后shrink_to_fit可以干掉。真正会伤害性能的是自定义迭代器没有实现size_hint导致collect不知道下界只能从很小容量开始反复扩容。如果这个迭代器产出很多元素扩容次数可能达到 20 次甚至更多每次都要重新分配内存并拷贝旧数据。我的建议就是自己实现迭代器时如果长度可得或者可估计务必重写size_hint。标准库里凡是你见过的类型Vec、Range、HashMap都实现了这个它们的性能表现是有保障的自定义迭代器往往是最容易埋性能坑的地方。5.3 什么时候手写循环反而更快迭代器链也不总是最优。有一种情况我建议直接写循环需要频繁基于索引随机访问多个集合时。比如你要同时根据i索引从三个不同Vec里取值做某种组合计算for i in 0..n { let a vec_a[i]; let b vec_b[i]; let c vec_c[i]; // 处理 }如果非要用迭代器会变成for ((a, b), c) in vec_a.iter().zip(vec_b.iter()).zip(vec_c.iter()) { // 处理 }这种zip嵌套虽然可行但可读性没那么直观。而且当编解码器试图做向量化优化时基于索引的三数组访问往往比嵌套zip的迭代器链更容易被编译器识别为“可以矢量化”的模式。类似的情况还有需要提前跳出循环并且还要知道当前索引的时候。迭代器能用enumerate配合break做到但手写循环读起来更像“算法描述稿”。性能上没有天壤之别但代码意图更清晰。我个人的习惯是迭代器链能表达得清楚就用迭代器一旦逻辑变得需要多个索引、多个条件提前终止、或者需要对不同位置的值做随机组合就退回去写循环。这里没有绝对的高下之分实际项目中以可维护性优先。6. 四两拨千斤的配合IntoIterator 与 FromIteratorIteratorTrait 不是孤军奋战的。标准库还设计了两个与之紧密相关的 TraitIntoIterator和FromIterator。前者让“可遍历”这件事有了统一入口后者让“可收集”这件事有了通用能力。6.1for循环的本质一切皆可into_iter()for x in expr的约束是expr必须实现IntoIterator。也就是说for循环并不直接要求类型实现Iterator而是先调用into_iter()把expr转成一个迭代器再在迭代器上调用next()。VecT实现了IntoIteratorVecT也实现了IntoIterator只是产生的元素类型不同前者是T后者是T。这就是为什么let v vec![1, 2, 3]; for x in v { /* x: i32 */ } for x in mut v { /* x: mut i32 */ } for x in v { /* x: i32 */ }这三种写法都能编译只是语义不同。很多从其他语言转来的初学者会觉得“为什么同一个 for 循环有三种行为”其实背后的机制就是IntoIterator的三种实现。6.2 为自己的类型实现IntoIterator假如我自己定义了一个包装类型struct Wrapper { items: Veci32, }如果想让for x in wrapper能直接遍历它的内部数据我需要为Wrapper实现IntoIteratorimpl IntoIterator for Wrapper { type Item i32; type IntoIter std::vec::IntoIteri32; fn into_iter(self) - Self::IntoIter { self.items.into_iter() } }这样之后let w Wrapper { items: vec![1, 2, 3] }; for x in w { // x: i32 }如果你还想支持for x in wrapper的形式就再为Wrapper实现一个impla IntoIterator for a Wrapper { type Item a i32; type IntoIter std::slice::Itera, i32; fn into_iter(self) - Self::IntoIter { self.items.iter() } }这个模式标准库广泛使用。理解它之后你就知道为什么很多第三方库的类型都能直接用for遍历了。6.3collect的底层FromIteratorcollect()方法在IteratorTrait 上定义它要求目标类型实现FromIteratorlet s: String [h, e, l, l, o].into_iter().collect(); let map: HashMapi32, i32 vec![(1, 2), (3, 4)].into_iter().collect(); let set: HashSeti32 vec![1, 2, 3, 2].into_iter().collect();可以看到String、HashMap、HashSet都实现了FromIterator。这意味着**collect不是只能收集成Vec**而是几乎所有常见容器都支持。如果你想把自己的自定义容器也变成collect的目标类型同样实现FromIterator。这个资产量很大但代码很模式化核心是用迭代器提供的next()逐个取元素并插入到自己的数据结构里。实现完FromIterator之后你的类型就天然能被iter().collect()收集这在构建 DSL 或数据转换管线时非常顺手。7. 方法论层面如何把迭代器用得更优雅最后谈点偏“软技能”的东西。迭代器链写得太多代码容易变得又长又绕。怎么在坚持函数式表达的同时保持可读性是我花了不少时间才摸索出来的。7.1 链过长时优先拆成中间变量看这段let result: Vec_ input.iter() .filter(|x| x 0) .map(|x| x.to_string()) .flat_map(|s| s.chars()) .take_while(|c| c.is_digit(10) || *c -) .collect();如果这一整条链有 20 行很多人看到后面已经忘了开头在干嘛。这时我会把它拆成两步到三步每步用一个语义化变量名let normalized: VecString input.iter() .filter(|x| x 0) .map(|x| x.to_string()) .collect(); let chars: Vecchar normalized.iter() .flat_map(|s| s.chars()) .take_while(|c| c.is_digit(10) || *c -) .collect();拆开之后每段的“输入是什么、输出是什么”都特别清楚而且调试时可以在中间打印结果。运行时性能差别可以忽略因为最终collect还是要做一遍遍历拆成两次collect也只多一次分配空间开销可控。7.2 善用Option相关方法简化逻辑迭代器的元素类型经常是Option或Result因此Iterator提供了一些专门针对它们的适配方法filter_map过滤掉None同时解包Some。很多时候比filter(...).map(...)更紧凑。flatten把嵌套的Option或迭代器拍平。while_let_some比较新的标准库方法按迭代器驱动的while let模式。拿一个需求举例解析一批字符串为整数跳过失败的。let parsed: Veci32 numbers.iter() .filter_map(|s| s.parse::i32().ok()) .collect();这个写法比先filter判断is_ok再map取出unwrap要安全优雅得多后者一不小心就会写出“unwrap 到 panic”的隐患代码。7.3 复杂度控制什么时候停止用迭代器迭代器不是越高阶越好。如果一段逻辑用迭代器表达过后你发现自己需要花五秒钟以上才能看懂那这段代码就已经越界了。我给自己定的几条规则链的层数不超过 4 层filter、map、flat_map、take这种算一层。闭包体超过 5 行时把闭包提出来命名成一个小函数。需要大量状态交互时比如维护累加器的同时还要决定是否跳过若干元素改用fold或直接写循环别硬套map嵌套。fold其实是迭代器里“万能兜底”的方法。它能表达几乎任何消耗性迭代逻辑而且语义是显式的。比如统计奇数的个数let odd_count (0..100).fold(0, |acc, x| acc (x % 2));这个简短又清晰。当然如果更简单直接count()加filter也行let odd_count (0..100).filter(|x| x % 2 1).count();两种都是好方案前者更像“原地累加”后者更像“过滤后统计”看团队代码风格。没有唯一正解重点是写出来的代码别人一眼能懂。7.4 记住Iterator是“可变的”最后再分享一个我自己的体会。Iterator::next()接收的是mut self这意味着迭代器本身是可变状态机。如果你在循环中提前break这个迭代器就停留在中途状态之后如果继续调用next()它会从上次的位置继续推进而不是从头开始。let mut iter (0..100).filter(|x| x % 3 0); assert_eq!(iter.next(), Some(0)); assert_eq!(iter.next(), Some(3)); // ... 中途 break 之后 let item iter.next(); // 继续从上次位置取这种语义在某些场景比如分页拉取、断点续读非常有用因为你可以把迭代器存起来下次接着用。但如果你写代码时误以为它每次都是重新开始的就会得到奇怪的结果。迭代器是有记忆的每次next()都会推进状态。展开for循环时你很容易忽略这一点但当你手动持有迭代器时它就是你手中的一个活状态变量。理解了这一点之后我发现很多并发/异步场景里也能用到迭代器比如多个协程轮流从一个共享迭代器里取任务只要保证内部状态是互斥的它就是一个天然的“任务分发器”。这也是 Rust 迭代器设计比较精妙的地方——本质其实很简单就是一个可推进、可暂停、可恢复的状态机。