16、迭代器模式:容器遍历、统一接口、链表迭代器实现
迭代器模式,说白了就是给容器遍历定个规矩。
我刚开始写C代码那会儿,遍历链表、数组、树,每个容器都得写一套不同的循环逻辑。后来维护一个老项目,光遍历方式就有七八种,改个需求得翻遍所有遍历代码。嗯,那时候我就意识到——遍历这件事,必须统一。
什么是迭代器模式?
迭代器模式的核心思想很简单:把遍历行为从容器中抽离出来。你不需要关心底层是数组还是链表,只需要调用统一的 next()、hasNext() 接口就能拿到下一个元素。
说白了,就是给容器配个“导游”。你跟着导游走就行,不用管路怎么修、车怎么开。
核心三要素:
- 容器(Aggregate):负责存储数据,提供创建迭代器的接口
- 迭代器(Iterator):封装遍历逻辑,提供统一的访问接口
- 客户端(Client):通过迭代器访问容器元素,不直接操作容器内部
为什么C语言也需要迭代器?
你可能会问:C语言又不是面向对象,搞什么设计模式?
我在项目中遇到过好几次这样的场景:一个模块用链表存数据,另一个模块用动态数组。后来业务变了,要把链表改成数组。结果呢?所有遍历代码全得重写。改一处还好,改几十处就真要命了。
迭代器模式解决的就是这个问题——遍历逻辑与容器解耦。你换容器,迭代器接口不变,客户端代码一行都不用改。
| 场景 | 没有迭代器 | 有迭代器 |
|---|---|---|
| 链表遍历 | while(p) { p = p->next; } | while(it.hasNext(&it)) { it.next(&it); } |
| 数组遍历 | for(i=0; i| while(it.hasNext(&it)) { it.next(&it); } |
|
| 树遍历 | 递归/栈模拟 | while(it.hasNext(&it)) { it.next(&it); } |
看到没?接口统一了,代码可维护性直接上一个台阶。
链表迭代器的实现
咱们直接上代码。我习惯用结构体+函数指针的方式来实现C语言的迭代器模式。这样既保留了C的简洁,又有了面向对象的味道。
// 链表节点
typedef struct Node {
int data;
struct Node* next;
} Node;
// 链表容器
typedef struct {
Node* head;
int size;
} LinkedList;
// 迭代器结构体
typedef struct {
Node* current; // 当前节点
int index; // 当前索引(可选)
int (*hasNext)(struct LinkedListIterator* self);
int (*next)(struct LinkedListIterator* self);
} LinkedListIterator;
// 判断是否还有下一个元素
int hasNext(LinkedListIterator* self) {
return self->current != NULL;
}
// 获取下一个元素并移动指针
int next(LinkedListIterator* self) {
if (self->current == NULL) {
return -1; // 或者用错误码
}
int data = self->current->data;
self->current = self->current->next;
self->index++;
return data;
}
// 创建迭代器
LinkedListIterator createIterator(LinkedList* list) {
LinkedListIterator it;
it.current = list->head;
it.index = 0;
it.hasNext = hasNext;
it.next = next;
return it;
}
我的习惯:迭代器里放函数指针,这样每个迭代器实例可以有自己的行为。比如你可以实现一个“过滤迭代器”,只返回偶数元素,接口完全不变。
使用示例
int main() {
LinkedList list;
// 初始化链表并添加数据...
LinkedListIterator it = createIterator(&list);
while (it.hasNext(&it)) {
int value = it.next(&it);
printf("%d ", value);
}
return 0;
}
你看,客户端代码完全不知道底层是链表还是数组。它只关心 hasNext 和 next 两个接口。这就是迭代器模式的魅力。
迭代器模式的知识体系
下面这张图是我自己总结的迭代器模式核心逻辑,你看一眼就能明白整个架构:
避坑指南
我曾经踩过的坑:
- 迭代器失效问题:遍历过程中如果修改了容器(比如删除当前节点),迭代器指针就悬空了。我建议遍历时只读不改,真要改就标记后统一处理。
- 多线程遍历:迭代器不是线程安全的。如果多个线程同时遍历同一个容器,加锁是必须的。我习惯在迭代器里加个“版本号”,容器修改时版本号递增,迭代器检测到版本变化就报错。
- 内存管理:迭代器本身不拥有数据,别在迭代器里 free 节点。谁创建谁释放,这个原则不能乱。
什么时候用迭代器模式?
我个人总结了几条经验:
- 容器类型可能变化:今天用链表,明天可能改数组。迭代器帮你隔离变化。
- 需要多种遍历方式:正序遍历、逆序遍历、过滤遍历。每种遍历写一个迭代器,互不干扰。
- 遍历逻辑复杂:比如树的层序遍历、图的深度优先遍历。把这些逻辑封装到迭代器里,客户端调用就一行 while 循环。
一个小技巧:如果你觉得写迭代器太麻烦,可以先写一个简单的 for 循环。等发现这段遍历代码被复制粘贴了三次以上,再重构出迭代器。别过度设计,但也不要等到代码烂成一锅粥才动手。
迭代器模式,说白了就是给遍历这件事定个标准。你想想看,如果每个容器都有一套自己的遍历方式,那代码得多乱?统一接口之后,不管是链表、数组还是树,客户端代码都长一个样。维护起来轻松多了。
嗯,这一章就到这里。记住:遍历是小事,统一是大事。