文章首发于知乎专栏:
https://zhuanlan.zhihu.com/c_1136684995157577728
个人公众号TarysThink
上一篇迭代器模式的文章,讲了C语言中关于数据结构遍历最常用的手法,掌握了上篇基本可以成为一个在这个方面的老手了,本篇再深入讲一下上篇所谓迭代器模式与设计模式里的迭代器模式还有多少差距。
迭代器模式提供了一套遍历数据结构的方法,这个方法能很好地分离数据结构与行为,从这个角度,我们上篇的讲的内容被称为“迭代器模式”也无可厚非。SayNice和SayHot我们忽略,再回看上一篇的代码:
typedef void (*Say)(Node pos);
ForeachStudent(Say say)
{
Node head = GetHeadNode();//获取头节点
Node pos = head;
ListForeach(head, pos)//遍历链表操作的封装
{
say(pos);
}
}
按照迭代器的思想,首先要把GetHeadNode()隐藏起来,其次要把ListForeach()函数做二次封装。
我们必须知道究竟遍历的是哪个数据结构,所以如果我们要隐藏GetHeadNode(),我们必须用其他方式获取这个信息。我们新增这么一个数据结构:
typedef struct StudentSet
{
Node (*fGetHeadNode)();
}StudentSet;
//结构初始化
StudentSet studentSet;
studentSet.fGetHeadNode = GetHeadNode;
然后代码变成了这个样子:
ForeachStudent(Say say)
{
StudentSet *studentSet = GetStudentSet();//获取头节点
Node pos = NULL;
ListForeach(studentSet->fGetHeadNode(), pos)//遍历链表操作的封装
{
say(pos);
}
}
有人看完会说,这就只是加了一层函数调用嘛,没什么意思。实际上这个操作是加了一层抽象,它使ForeachStudent不再依赖于具体的获取头节点的函数,将来链表换成数组,只需要在studentSet初始化的时候赋值为GetArrayFirst()函数即可。当然StudentSet里还可以添加fGetLastNode()、fGetNextNode()等等行为。
接下来解决第二个问题,二次封装ListForeach()函数。
ForeachStudent()函数不想看到数据结构遍历方式,那么StudentSet看到怎么样?尝试写一下代码:
typedef struct StudentSet
{
Node (*fGetHeadNode)();
void (*fForeach)(struct StudentSet *studentSet);
Node pos;
}StudentSet;
//结构初始化
StudentSet studentSet;
studentSet.fGetHeadNode = GetHeadNode;
studentSet.fForeach = ListForeach;
//遍历时
ForeachStudent(Say say)
{
StudentSet *studentSet = GetStudentSet();//获取头节点
studentSet->fForeach(studentSet)//遍历链表操作的封装
{
say(studentSet->pos);
}
}
看懂上面这段代码后,一定会有人问,ListForeach和函数指针fForeach的入参都不一样了。其实没变多少,原来的临时变量pos存到了StudentSet里,ListForeach改一下定义,内部通过studentSet跳转一次到pos即可。
到这里,遍历函数ForeachStudent()已经完全感知不到链表的存在了。迭代器模式的目标已经完全达成了。我认为C软件设计做到这个程度已经足够了。《设计模式》里的迭代器模式,还对遍历函数ListForeach做了进一步抽象,进而区分开容器和迭代器,此处不细谈。