集合是编程中最常使用的数据类型之一。尽管如此,集合只是一组对象的容器而已。
大部分集合使用简单列表存储元素。但有些集合还会使用栈、树、图和其他复杂的数据结构。
无论集合的构成方式如何,它都必须提供某种访问元素的方式,便于其他代码使用其中的元素。集合应提供一种能够遍历元素的方式,且保证它不会周而复始地访问同一个元素。
如果你的集合基于列表,那么这项工作听上去仿佛很简单。但如何遍历复杂数据结构(例如树)中的元素呢?例如,今天你需要使用深度优先算法来遍历树结构,明天可能会需要广度优先算法,下周则可能会需要其他方式(比如随机存取树中的元素)。
不断向集合中添加遍历算法会模糊其 “高效存储数据” 的主要职责。此外,有些算法可能是根据特定应用订制的,将其加入泛型集合类中会显得非常奇怪。
另一方面,使用多种集合的客户端代码可能并不关心存储数据的方式。不过由于集合提供不同的元素访问方式,你的代码将不得不与特定集合类进行耦合。
解决方案:
迭代器模式的主要思想是将集合的遍历行为抽取为单独的迭代器对象。
除实现自身算法外,迭代器还封装了遍历操作的所有细节,例如当前位置和末尾剩余元素的数量。因此,多个迭代器可以在相互独立的情况下同时访问集合。
迭代器通常会提供一个获取集合元素的基本方法。客户端可不断调用该方法直至它不返回任何内容,这意味着迭代器已经遍历了所有元素。
所有迭代器必须实现相同的接口。这样一来,只要有合适的迭代器,客户端代码就能兼容任何类型的集合或遍历算法。如果你需要采用特殊方式来遍历集合,只需创建一个新的迭代器类即可,无需对集合或客户端进行修改。
真实世界类比:
你计划在罗马游览数天,参观所有主要的旅游景点。但在到达目的地后,你可能会浪费很多时间绕圈子,甚至找不到罗马斗兽场在哪里。
或者你可以购买一款智能手机上的虚拟导游程序。这款程序非常智能而且价格不贵,你想在景点待多久都可以。
第三种选择是用部分旅行预算雇佣一位对城市了如指掌的当地向导。向导能根据你的喜好来安排行程,为你介绍每个景点并讲述许多激动人心的故事。这样的旅行可能会更有趣,但所需费用也会更高。
所有这些选择(自由漫步、智能手机导航或真人向导)都是这个由众多罗马景点组成的集合的迭代器。
(1)模式动机
在软件构建过程中,集合对象内部结构常常变化各异。但对于这些集合对象,我们希望在不暴露其内部结构的同时,可以让外部客户代码透明地访问其中包含的元素,同时这种 “透明遍历” 也为 “同一种算法在多种集合对象上进行操作” 提供了可能。
如何将 “客户代码与复杂的对象容器结构” 解耦,让对象容器自己来实现自身的复杂结构,从而使得客户代码就像处理简单对象一样来处理复杂的对象容器?
(2)模式定义
使用面向对象技术将这种遍历机制抽象为 “迭代器对象” 为 “应对变化中的集合对象” 提供了一种优雅的方式。
(3)要点总结
a). 迭代抽象:访问一个聚合对象的内容而无需暴露它的内部表示。
b). 迭代多态:为遍历不同的集合结构提供一个统一的接口,从而支持同样的算法在不同的集合结构上进行操作。
c). 迭代器的健壮性考虑:遍历的同时更改迭代器所在的集合结构,会导致问题。
下列是 GoF 传统迭代器伪代码示例,传统的迭代器模式(其使用的是面向对象的技术)我们已经抛弃,我们更多用的是泛型编程。
template<typename T>
class Iterator{
public:
virtual void first() = 0;
virtual void next() = 0;
virtual bool isDone() const = 0;
virtual T& current() = 0;
};
template<typename T>
class MyCollection{
public:
Iterator<T> GetIterator(){
//...
}
};
template<typename T>
class CollectionIterator : public Iterator<T>{
MyCollection<T> mc;
public:
CollectionIterator(const MyCollection<T> &c) : mc(c) {}
void first() override {}
void next() override {}
bool isDone() const override {}
T& current() override {}
};
void MyAlgorithm(){
MyCollection<int> mc;
Iterator<int> iter = mc.GetIterator();
for (iter.first(); != iter.isDone(); iter.next()){
cout << iter.current() << endl;
}
}
因为传统的迭代器有很多缺点,最核心的缺点就是面向对象使用了虚函数去调用,如上述代码使用的for (iter.first(); != iter.isDone(); iter.next())
,虚函数调用是有性能成本的,因为要绕一个虚函数的表指针然后再找到函数地址,也就是经过了二次指针的间接运算,一旦 for循环
执行很多次的话性能就变差了。首先泛型编程的迭代器用的是模板来实现,是编译时多态,而虚函数是运行时多态,编译时多态是在预编译的过程就已经做了,在用的时候直接调就行;其次泛型编程有很多的迭代器,如传统的迭代器只支持往前走,而泛型的可支持往后走,且可以指定走多少步。但是在Java、C#之类的,他们用的还是运行时依赖,所以性能就差一些。