01_迭代器模式 Iterator Pattern
1. 基本信息
中文名称: 迭代器模式
英文名称: Iterator Pattern
模式类型: 行为型设计模式
Rust 中常见实现方式: Iterator trait、iter()、into_iter()、iter_mut()、map()、filter()、collect()、for 循环
迭代器模式的核心是:把“如何遍历集合”的过程从集合本身中抽离出来,让使用者不需要关心集合内部结构,也能按照统一方式访问其中的元素。
在 Rust 中,迭代器不是一个简单的“遍历工具”,而是一套非常重要的数据处理机制。它不仅可以遍历数组、向量、哈希表等集合,还可以通过链式调用完成过滤、映射、聚合、查找、转换等操作。
2. 这个模式解决什么问题
迭代器模式主要解决的是:如何在不暴露集合内部结构的情况下,统一访问集合中的元素。
在实际开发中,不同数据结构的内部组织方式可能完全不同。例如:
1. Vec 是连续数组结构
2. HashMap 是哈希表结构
3. LinkedList 是链表结构
4. 文件内容可能是一行一行读取
5. 网络数据可能是一段一段到达
如果每种数据结构都让使用者自己关心内部访问逻辑,那么调用代码会变得复杂,而且会和具体数据结构强绑定。
迭代器模式通过引入一个统一的访问接口,使调用者只需要关心:
是否还有下一个元素?
如何取得当前元素?
取得元素后如何继续处理?
而不需要关心:
数据到底存在哪里?
底层是数组、链表还是哈希表?
遍历时下标如何变化?
内部节点如何移动?
因此,迭代器模式的本质是:把“遍历逻辑”从“数据结构本身”中抽离出来,让数据访问过程更加统一、灵活和可组合。
迭代器模式的作用就是:提供一个统一的访问接口,使调用方只关心“下一个元素是什么”,而不关心“这个元素是怎么被取出来的”。
3. 不使用这个模式会怎样
如果不使用迭代器模式,代码中可能会出现大量直接依赖下标、索引、内部节点或具体存储结构的遍历逻辑。
例如,遍历数组时可能使用下标,遍历链表时可能操作指针,遍历树时可能写递归。这样每种数据结构都有一套不同的访问方式。
这会导致几个问题:
1. 遍历逻辑和数据结构强耦合。
2. 调用方需要知道集合内部如何存储数据。
3. 数据处理流程不统一,代码可读性下降。
4. 后续替换集合类型时,遍历代码也要跟着改。
5. 筛选、映射、统计等操作容易写成重复代码。
在 Rust 中,如果完全不用迭代器思想,就容易写出很多手动循环、临时变量和重复判断逻辑。代码虽然能运行,但表达力会比较弱。
4. 传统面向对象中的实现思路
在传统面向对象语言中,迭代器模式通常会把集合对象和迭代器对象分开。
一般会定义两个角色:
聚合对象:保存一组数据,例如 List、Set、Tree
迭代器对象:负责遍历聚合对象中的元素
典型结构是:
Aggregate 接口
└── createIterator()
Iterator 接口
├── hasNext()
└── next()
也就是说,集合对象本身不直接暴露内部结构,而是提供一个创建迭代器的方法。调用者通过迭代器对象访问集合中的元素。
传统面向对象语言中的迭代器模式往往以“对象 + 接口”的形式存在。也就是说,迭代器本身通常是一个独立对象,外部通过接口调用它的方法来完成遍历。
这种设计的好处是,调用者不需要知道集合内部是数组、链表还是树结构,只需要通过统一的迭代器接口进行访问。
传统迭代器模式强调的是:
封装集合内部结构
提供统一遍历接口
将遍历行为从集合对象中分离出来
例如在 Java 中,Iterator 通常包含 hasNext() 和 next() 方法。调用者先判断是否存在下一个元素,再取出元素。
5. Rust 中的实现思路
Rust 中已经把迭代器模式内置到了语言和标准库中。
Rust 的迭代器核心是 Iterator trait。只要一个类型实现了 Iterator,它就可以被统一地遍历和处理。
Rust 的迭代器核心可以理解为:一个对象只要能够不断产生下一个元素,它就可以是一个迭代器。
Rust 中常见的迭代器使用方式有三种:
iter():按不可变引用遍历,不取得所有权
iter_mut():按可变引用遍历,可以修改元素
into_iter():取得所有权,把集合中的元素移动出来
这三个方法非常重要,因为它们和 Rust 的所有权机制直接相关。
在 Rust 中,迭代器不仅用于“遍历”,还常用于构建数据处理流程。比如:
遍历
筛选
转换
查找
统计
聚合
收集
链式处理
Rust 的迭代器更像是一种“数据流处理方式”。它可以把一个集合经过多个步骤处理后,得到新的结果。
比如可以理解成:
原始数据
→ 遍历
→ 过滤
→ 转换
→ 收集
→ 得到结果
因此,在 Rust 中学习迭代器模式,不只是学会 for 循环,而是要理解:Rust 倾向于用迭代器链来表达数据处理过程。
6. Rust 中涉及的语言特性
迭代器模式在 Rust 中主要涉及以下语言特性:
Iterator trait
for 循环
闭包
泛型
所有权
借用
生命周期
Option
collect
适配器方法
其中最核心的是 Iterator trait。
Iterator 的基本思想是:每次调用时返回下一个元素。如果还有元素,就返回 Some(value);如果没有元素,就返回 None。
所以迭代器和 Option 也有密切关系。
可以这样理解:
Iterator 负责“不断产生下一个元素”
Option 负责表达“还有元素”或“没有元素”
闭包负责定义筛选、转换等处理逻辑
collect 负责把处理结果重新收集成集合
Rust 的迭代器还有一个重要特点:惰性求值。
也就是说,很多迭代器操作本身不会立刻执行,只有当真正需要结果时才会开始计算。比如 map、filter 只是描述处理过程,真正触发执行的通常是 collect、for、sum、count 等操作。
7. 性能特点
Rust 的迭代器模式经常被称为一种零成本抽象。
所谓零成本抽象,并不是说它完全没有任何成本,而是说:使用高级抽象写出来的代码,在经过编译器优化后,通常可以接近手写循环的性能。
Rust 迭代器性能好的原因主要有几个。
第一,很多迭代器使用泛型和静态分发。编译器在编译期就知道具体迭代器类型,可以进行内联和优化。
第二,迭代器通常是惰性执行的。map、filter 等操作不会立即生成中间集合,只有在最终消费时才逐个处理元素。
第三,迭代器链在优化后可能被编译器合并成类似普通循环的机器代码,从而减少函数调用和中间变量开销。
第四,Rust 的所有权和借用检查发生在编译期,不需要运行时垃圾回收来管理迭代过程中的对象生命周期。
但是也要注意,迭代器并不是在任何情况下都一定比手写循环快。
例如:
大量使用 Box<dyn Iterator> 可能引入动态分发开销
过度复杂的迭代器链可能降低可读性
collect 会真正分配新的集合
debug 模式下迭代器优化效果不如 release 模式明显
所以在 Rust 中,迭代器通常既有较好的表达能力,也能保持较高性能。但在性能敏感场景下,仍然需要通过测试和基准分析来判断。
一句话总结:
Rust 迭代器的优势是:写法高级,但编译后通常接近底层循环。
8. 典型应用场景
迭代器模式适合用于所有需要“逐个处理数据”的场景。
常见应用包括:
1. 遍历数组、Vec、HashMap 等集合
2. 对集合中的元素进行筛选
3. 对数据进行批量转换
4. 统计符合条件的元素数量
5. 查找某个元素
6. 对文本进行逐行处理
7. 对文件内容进行流式处理
8. 对数据库查询结果进行逐条处理
9. 构建数据处理管道
10. 对错误结果进行统一收集
在真实 Rust 项目中,迭代器经常出现在这些地方:
命令行工具中的参数处理
文本解析
日志分析
数据清洗
文件扫描
网络请求结果处理
编译器或解释器中的 token 流处理
爬虫数据处理
批量任务调度
比如做 Rust crate 批量编译实验时,如果有很多 crate 需要分批处理,那么 chunks()、enumerate()、filter()、map() 这些方法本质上都属于迭代器模式的应用。
9. 和相似模式的区别
迭代器模式容易和过滤器模式、访问者模式、责任链模式混淆。
1. 迭代器模式和过滤器模式
二者关系很近。
迭代器模式关注“如何逐个访问元素”。
过滤器模式关注“如何筛选符合条件的元素”。
在 Rust 中,过滤器模式经常通过迭代器的 filter() 方法实现。
所以可以说:
过滤器模式通常建立在迭代器模式之上。
2. 迭代器模式和访问者模式
二者都可能用于遍历复杂结构,但关注点不同。
迭代器模式关注遍历顺序和元素访问。
访问者模式关注在不修改数据结构的情况下增加新的操作。
比如遍历一个语法树时,迭代器关心“如何访问每个节点”,访问者关心“访问到节点后执行什么操作”。
在 Rust 中,如果数据结构是 enum 表达的,很多时候直接用 match 就可以替代一部分访问者模式。
3. 迭代器模式和责任链模式
二者都可能表现为“一个数据经过多个步骤”。
但是:
迭代器模式处理的是一组元素的连续访问。
责任链模式处理的是一个请求经过多个处理者。
迭代器偏向数据集合处理,责任链偏向请求流程处理。
10. 使用该模式的优点
迭代器模式的优点主要有以下几个。
第一,隐藏集合内部结构。调用方不需要知道集合底层是数组、链表、哈希表还是其他结构。
第二,提供统一遍历方式。不同集合都可以用类似方式进行访问。
第三,提升代码表达力。Rust 的迭代器链可以清晰表达“先筛选、再转换、最后收集”的过程。
第四,减少重复代码。很多常见操作已经由标准库提供,不需要自己手写循环。
第五,便于组合。map、filter、take、skip、enumerate 等方法可以自由组合,形成数据处理管道。
第六,支持惰性求值。中间过程不会立即执行,可以减少不必要的计算。
第七,符合 Rust 的所有权模型。通过 iter()、iter_mut()、into_iter() 可以明确区分借用、可变借用和所有权移动。
11. 使用该模式的代价
迭代器模式虽然很常用,但也有一些代价。
第一,对于初学者来说,迭代器链一开始不如普通 for 循环直观。
第二,链式调用过长时,可读性会下降。
第三,涉及引用和所有权时,可能会遇到类型不匹配、借用层级过多等问题。
第四,闭包中的引用关系有时不容易一眼看清。
第五,复杂业务逻辑强行写成一长串迭代器链,反而会让代码难以调试。
所以迭代器并不是为了替代所有循环。简单循环可以用 for,清晰的数据转换流程适合用迭代器链。
12. 什么时候不应该使用
以下情况不适合强行使用复杂迭代器链:
1. 业务逻辑有很多分支判断。
2. 每一步都需要复杂的状态变化。
3. 中间过程需要频繁调试。
4. 迭代器链太长,导致阅读困难。
5. 普通 for 循环已经足够清晰。
在 Rust 中,不应该为了显得“函数式”而把所有逻辑都写成链式调用。
可以遵循一个简单原则:
如果数据处理过程是线性的、清晰的,用迭代器链。
如果过程有复杂状态和多层分支,用普通 for 循环。
13. 一个简单例子思路
可以设计这样一个例子来理解迭代器模式:
有一组学生成绩,需要完成以下处理:
1. 遍历所有学生成绩。
2. 筛选出及格的学生。
3. 把分数转换成等级。
4. 统计每个等级的人数。
5. 输出最终结果。
这个例子可以体现迭代器模式的完整流程:
数据源
→ 遍历
→ 过滤
→ 转换
→ 统计
→ 输出
在 Rust 中,可以使用 iter() 访问学生列表,使用 filter() 筛选及格学生,使用 map() 转换成绩等级,最后用 collect() 或统计方法得到结果。
这个例子适合用来练习:
iter()
filter()
map()
collect()
count()
fold()
14. 总结一句话
迭代器模式的本质是:
把“遍历集合的方式”从集合内部抽离出来,让调用方用统一的方式访问和处理数据,而不需要关心集合的内部结构。
在 Rust 中,迭代器模式不仅是一种设计模式,更是一种非常核心的数据处理思想。它把遍历、筛选、转换、收集等操作组织成清晰的数据流,使代码更简洁、更安全,也更符合 Rust 的表达方式。