1 意图
提供一种方法顺序访问一个聚合对象中的各个元素,而又不需暴露该对象的内部表示。
2 别名
游标(Curse)
3 动机
一个聚合对象,如列表,应该提供一种方法来让别人可以访问它的元素,而又不需暴露它的内部结构,此外,针对不同的需要,可能要以不同的方式遍历这个列表。但是即使可以预见到所需的那些遍历操作,你可能也不希望列表的接口中充斥着各种不同遍历的操作。有时还可能需要在同一个列表上同时进行多个遍历。
迭代器模式都可帮你解决所有这些问题。这一模式的关键思想是将对列表的访问和遍历从列表对象中分离出来并放入一个迭代器对象中,迭代器类定义了一个访问该列表元素的接口。迭代器对象负责跟踪当前的元素,即它知道哪些元素已经遍历过了。
例如,一个列表(List)类可能需要一个列表迭代器,它们之间的关系如下图:
在实例化列表迭代器之前,必须提供遍历的列表。一旦有了该列表迭代器的实例,就可以顺序地访问该列表的各个元素,CurrentItem操作返回表列中的当前元素,First操作初始化迭代器,使当前元素指向列表的第一个元素 ,Next操作将当前元素指针向前推进一步,指向下一个元素, 而IsDone检查是否已越过最后一个元素,也就是完成了此次遍历。
将遍历机制与列表对象分离使我们可以定义不同的迭代器来实现不同的遍历策略,而无需在列表接口中列举它们。例如 , 过滤表列迭代器(FilteringListIterator)可能只访问那些满足特定过滤约束条件的元素。
注意迭代器和列表是耦合在一起的,而且客户对象必须知道遍历的是一个列表而不是其他聚合结构。 最好能有一种办法使得不需改变客户代码即可改变该聚合类。可以通过将迭代器的概念推广到多态迭代(polymorphic iteration)来达到这个目标。
例如, 假定我们还有一个列表的特殊实现,比如说SkipList[Pug90 ]。SkipList是一种具有类似于平衡树性质的随机数据结构。我们希望我们的代码对List和SkipList对象都适用。
首先,定义一个抽象列表类 AbstractList,它提供操作列表的公共接口。类似地,我们也需要一个抽象的迭代器类Iterator,它定义公共的迭代接口。然后我们可以为每个不同的列表实现定义具体的Iterator子类。这样迭代机制就与具体的聚合类无关了。
余下的问题是如何创建迭代器。既然要使这些代码不依赖于具体的列表子类 , 就不能仅仅简单地实例化一个特定的类 , 而要让列表对象负责创建相应的迭代器。这需要列表对象提供CreateIterator这样的操作, 客户请求调用该操作以获得一个迭代器对象。
创建迭代器是一个Factory Method模式(3.3)的例子,我们在这里用它来使得一个客户可向一个列表请求合适的迭代器。Factory Method模式产生两个类层次,一个是列表的,一个是迭代器的。CreateIterator是联系这两个类层次的。
4 适用性
迭代器模式可用来:
- 1 访问一个聚合对象的内容而无需暴露它的内部表示;
- 2 支持对聚合对象的多种遍历;
- 3 为遍历不同的聚合结构提供一个统一的接口 (即, 支持多态迭代)
5 结构
6 参与者
- Iterator(迭代器)
——迭代器定义访问和遍历元素的接口 - concreteIterator(具体迭代器)
——具体迭代器实现迭代器接口
—— 对该聚合遍历时跟踪当前位置 - Aggregate(聚合)
—— 聚合定义创建相应迭代器对象的接口 - ConcreteAggregate(具体聚合)
——具体聚合实现创建相应迭代器的接口,该操作返回concreteIterator的一个适当的实例。
7 协作
concreteIterator跟踪聚合中的当前对象,并能够计算出待遍历的后继对象。
8 效果
迭代器模式有三个重要的作用:
- 1 它支持以不同的方式遍历一个聚合复杂的聚合可用多种方式进行遍历。例如 , 代码生成和语义检查要遍历语法分析树。代码生成可以按中序或者按前序来遍历语法分析树。迭代器模式使得改变遍历算法变得很容易 : 仅需用一个不同的迭代器的实例代替原先的实例即可。你也可以自己定义迭代器的子类以支持新的遍历;
- 2 迭代器简化了聚合的接口有了迭代器的遍历接口,聚合本身就不再需要类似的遍历接口了。这样就简化了聚合的接口。
- 3 在同一个聚合上可以有多个遍历 每个迭代器保持它自己的遍历状态。因此你可以同时进行多个遍历。
9 实现
迭代器在实现上有许多变化和选择。下面是一些较重要的实现。实现迭代器模式时常需要根据所使用的语言提供的控制结构来进行权衡。一些语言 (例如, CLU[LG86])甚至直接支持这一模式。
- 1 谁控制该迭代 一个基本的问题是决定由哪一方来控制该迭代 , 是迭代器还是使用该迭代器的客户。当由客户来控制迭代时 , 该迭代器称为一个外部迭代器(external iterator),而当由迭代器控制迭代时, 该迭代器称为一个内部迭代器(internal iterator)。使用外部迭代器的客户必须主动推进遍历的步伐,显式地向迭代器请求下一个元素。相反地 , 若使用内部迭代器,客户只需向其提交一个待执行的操作,而迭代器将对聚合中的每一个元素实施该操作。
外部迭代器比内部迭代器更灵活。例如 , 若要比较两个集合是否相等,这个功能很容易用外部迭代器实现,而几乎无法用内部迭代器实现。在象 C + +这样不提供匿名函数、闭包 , 或象Smalltalk和CLOS这样不提供连续( continuation)的语言中,内部迭代器的弱点更为明显。但另一方面, 内部迭代器的使用较为容易, 因为它们已经定义好了迭代逻辑。 - 2 谁定义遍历算法 迭代器不是唯一可定义遍历算法的地方。聚合本身也可以定义遍历算法,并在遍历过程中用迭代器来存储当前迭代的状态。我们称这种迭代器为一个游标,因为它仅用来指示当前位置。客户会以这个游标为一个参数调用该聚合的Next操作,而Next操作将改变这个指示器的状态。
如果迭代器负责遍历算法 , 那么将易于在相同的聚合上使用不同的迭代算法 , 同时也易于在不同的聚合上重用相同的算法。从另一方面说 , 遍历算法可能需要访问聚合的私有变量。如果这样,将遍历算法放入迭代器中会破坏聚合的封装性。 - 3 迭代器健壮程度如何 在遍历一个聚合的同时更改这个聚合可能是危险的。如果在遍历聚合的时候增加或删除该聚合元素 , 可能会导致两次访问同一个元素或者遗漏掉某个元素。一个简单的解决办法是拷贝该聚合,并对该拷贝实施遍历 , 但一般来说这样做代价太高。
一个健壮的迭代器(robust iterator)保证插入和删除操作不会干扰遍历, 且不需拷贝该聚合。有许多方法来实现健壮的迭代器。其中大多数需要向这个聚合注册该迭代器。当插入或删除元素时,该聚合要么调整迭代器的内部状态 , 要么在内部的维护额外的信息以保证正确的遍历。 - 4 附加的迭代器操作迭代器的最小接口由First、Next、IsDone和CurrentItem 操作组成。其他一些操作可能也很有用。例如 , 对有序的聚合可用一个Previous操作将迭代器定位到前一个元素。SkipTo操作用于已排序并做了索引的聚合中,它将迭代器定位到符合指定条件的元素对象上。
- 5 在C + +中使用多态的迭代器使用多态迭代器是有代价的。它们要求用一个Factory Method动态的分配迭代器对象。因此仅当必须多态时才使用它们。否则使用在栈中分配内存的具体的迭代器。
多态迭代器有另一个缺点 : 客户必须负责删除它们。这容易导致错误 , 因为你容易忘记释放一个使用堆分配的迭代器对象,当一个操作有多个出口时尤其如此。而且其间如果有异常被触发的话,迭代器对象将永远不会被释放。 - 6 迭代器可有特权访问迭代器可被看为创建它的聚合的一个扩展。迭代器和聚合紧密耦合。在C + +中我们可让迭代器作为它的聚合的一个友元 (friend )来表示这种紧密的关系。这样你就不需要在聚合类中定义一些仅为迭代器所使用的操作。
但是, 这样的特权访问可能使定义新的遍历变得很难 , 因为它将要求改变该聚合的接口增加另一个友元。为避免这一问题 , 迭代器类可包含一些protected操作来访问聚合类的重要的非公共可见的成员。迭代器子类 (且只有迭代器子类)可使用这些protected操作来得到对该聚合的特权访问。 - 7 用于复合对象的迭代器在Composite ( 4 . 3 )模式中的那些递归聚合结构上 , 外部迭代器可能难以实现, 因为在该结构中不同对象处于嵌套聚合的多个不同层次, 因此一个外部迭代器为跟踪当前的对象必须存储一条纵贯该 Composite的路径。有时使用一个内部迭代器会更容易一些。它仅需递归地调用自己即可,这样就隐式地将路径存储在调用栈中,而无需显式地维护当前对象位置。如果复合中的节点有一个接口可以从一个节点移到它的兄弟节点、父节点和子节点 , 那么基于游标的迭代器是个更好的选择。游标只需跟踪当前的节点 ; 它可依赖这种节点接口来遍历该复合对象。复合常常需要用多种方法遍历。前序 , 后序, 中序以及广度优先遍历都是常用的。你可用不同的迭代器类来支持不同的遍历。
- 8 空迭代器一个空迭代器(NullIterator)是一个退化的迭代器 , 它有助于处理边界条件。根据定义,一个NullIterator总是已经完成了遍历:即, 它的IsDone操作总是返回true。空迭代器使得更容易遍历树形结构的聚合 (如复合对象)。在遍历过程中的每一节点 , 都可向当前的元素请求遍历其各个子结点的迭代器。该聚合元素将返回一个具体的迭代器。但叶节点元素返回NullIterator的一个实例。这就使我们可以用一种统一的方式实现在整个结构上的遍历。