设计模式22-迭代器模式
- 迭代器模式(Iterator Pattern)
- 动机
- 定义结构
- 定义
- 结构
- 结构图解释
- 注意事项
- C++代码推导
- 多态属性(虚函数)实现迭代器
- 1. **返回值问题**
- 2. **对象切割问题**
- 3. **内存管理问题**
- 4. **迭代器生命周期问题**
- 5. **接口设计问题**
- 6. **接口返回值的问题**
- 模版形式实现迭代器
- 为什么以模版形式实现迭代器而不使用多态性来实现迭代器
- 1. **性能考虑**
- 2. **灵活性和类型安全**
- 3. **类型擦除的避免**
- 4. **代码复用和泛型编程**
- 5. **减少内存开销**
- 6. **模板元编程**
- 什么时候选择多态性?
- 总结
- 优缺点
- 应用场景
- 总结
迭代器模式(Iterator Pattern)
动机
- 软件构建过程集合对象内部结构经常变化各异。但对于这些集合对象,我们希望在不暴露其内部结构的同时,也可以让外部客户代码透明的访问其中包含的元素,同时这种透明便利也为同一种算法在多种集合对象上进行操作提供了可能。
- 使用面向对象技术将这种便利机制抽象为迭代器对象。为应对变化中的集合对象提供了一种优雅的方式。
- 在软件开发中,经常需要遍历某种集合对象(如数组、链表、树等)中的元素。集合的具体实现通常不应暴露给客户端,以确保集合的封装性和灵活性。为了遍历集合中的元素而不暴露集合的内部结构,迭代器模式引入了一种通用的遍历机制,使得客户端可以遍历集合而不关心集合的内部实现。
定义结构
定义
迭代器模式提供一种方法,顺序访问一个聚合对象中的各个元素,而不暴露该对象的内部表示。
结构
结构图解释
1. 类和接口
-
Aggregate(聚合类):这是一个集合类,它提供了创建迭代器对象的方法(通常是
CreateIterator()
或类似的命名)。在这个例子中,Aggregate
类有一个Createlterator()
方法,该方法返回一个新的迭代器实例。这个迭代器实例是基于当前Aggregate
对象创建的,以便能够遍历它的元素。注意,图片中的Createlterator()
方法名似乎有一个小错误,应该是CreateIterator()
,但我将遵循图片中的实际文本。 -
lterator(迭代器接口):这是一个定义迭代器行为的接口。它声明了用于遍历元素的方法,如
First()
(定位到序列的第一个元素),Next()
(移动到序列中的下一个元素),IsDone()
(检查序列中是否还有更多的元素),以及CurrentItem()
(获取当前位置的元素)。在标准的命名习惯中,接口名通常是单数且以大写字母开头,但这里使用了lterator
,可能是一个打字错误,正确的可能是Iterator
。 -
Client(客户端类):这是使用迭代器遍历聚合类中的元素的类。它持有一个迭代器对象的引用,并通过这个迭代器来遍历聚合类中的元素。在这个例子中,
Client
类有一个名为lterator
的私有成员变量(可能是Iterator
的一个实例,但这里使用了lterator
),并使用了迭代器的First()
、Next()
和IsDone()
方法来遍历元素。
2. 实现类
-
ConcreteAggregate(具体聚合类):这是
Aggregate
类的一个具体实现,它实现了CreateIterator()
方法来返回一个Concretelterator
(应为ConcreteIterator
)实例。这个实例能够遍历ConcreteAggregate
对象中的元素。 -
Concretelterator(具体迭代器类):这是
lterator
(应为Iterator
)接口的一个具体实现。它实现了接口中定义的所有方法,包括First()
、Next()
、IsDone()
和CurrentItem()
,以便能够遍历ConcreteAggregate
对象中的元素。在这个例子中,Concreteelterator
的构造函数接收一个ConcreteAggregate
对象作为参数,以便它能够知道要遍历哪个集合。注意,这里的Concretelterator
也是一个小错误,正确的应该是ConcreteIterator
。
注意事项
- 图片中的类名和方法名中存在一些拼写错误,如
lterator
应该是Iterator
,Createlterator()
应该是CreateIterator()
,Currentitem()
应该是CurrentItem()
。 - 迭代器模式的核心在于将遍历聚合类对象的责任从聚合类本身转移到迭代器对象上,这有助于保持聚合类的简洁,并使得遍历逻辑可以在不同的迭代器之间重用。
希望这个解释能够帮助您理解这张关于迭代器模式的结构图。
C++代码推导
多态属性(虚函数)实现迭代器
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;}}
这种迭代器实现方式存在一些问题,使得它在实际应用中可能并不是最佳选择。以下是一些关键问题及原因:
1. 返回值问题
Iterator<int> iter = mc.GetIterator();
-
问题:
Iterator<int>
是一个抽象类,不能直接实例化。当你尝试返回一个Iterator<int>
对象时,编译器会报错,因为不能创建一个抽象类的实例。 -
改进:应返回一个指向
Iterator<int>
的指针或者使用智能指针,例如std::unique_ptr<Iterator<int>>
,以确保可以动态地分配一个具体的迭代器实例。std::unique_ptr<Iterator<T>> GetIterator() {return std::make_unique<CollectionIterator<T>>(*this); }
2. 对象切割问题
Iterator<int> iter = mc.GetIterator();
-
问题:即使
Iterator<int>
能够返回一个对象(假设Iterator<int>
不是抽象类),由于Iterator<int>
的拷贝会发生对象切割问题,导致派生类的部分(CollectionIterator<int>
的特定实现)丢失,剩下的只是一个基类的对象,这会使得运行时的多态行为失效。 -
改进:避免对象切割,返回迭代器的指针或引用。
3. 内存管理问题
-
问题:如果使用原始指针(如
Iterator<T>*
)返回具体迭代器,会面临内存管理问题。如果用户忘记释放内存,可能会导致内存泄漏。 -
改进:使用智能指针来管理内存,例如
std::unique_ptr
或std::shared_ptr
。智能指针会自动管理对象的生命周期,避免内存泄漏。
4. 迭代器生命周期问题
-
问题:
CollectionIterator
持有MyCollection
的副本(通过成员变量mc
)。这意味着迭代器中保存的集合与原集合可能是两个不同的对象。如果集合发生改变,迭代器将无法反映这些变化。 -
改进:迭代器应持有集合的引用而不是副本,以确保它们始终引用相同的集合对象。
template<typename T> class CollectionIterator : public Iterator<T> {const MyCollection<T>& mc; public:CollectionIterator(const MyCollection<T>& c) : mc(c) { }// 实现迭代器方法 };
5. 接口设计问题
-
问题:迭代器接口设计可能存在一些细节问题。例如,
current()
方法返回对当前元素的引用时,如果集合为空,调用current()
会导致未定义行为。 -
改进:在实现
current()
时,应该确保集合非空或抛出异常以避免未定义行为。也可以提供一个安全的接口,如返回指向元素的指针。
6. 接口返回值的问题
-
问题:
GetIterator()
方法在你的设计中返回Iterator<T>
,但是实际应该返回指向具体Iterator
的指针或引用,以支持多态。 -
改进:改用返回智能指针或指针以支持多态:
std::unique_ptr<Iterator<T>> GetIterator() {return std::make_unique<CollectionIterator<T>>(*this); }
模版形式实现迭代器
以下是一个使用迭代器模式遍历一个简单集合(例如整数数组)的C++代码示例。
迭代器接口类:
template <typename T>
class Iterator {
public:virtual ~Iterator() {}virtual T first() = 0;virtual T next() = 0;virtual bool isDone() const = 0;virtual T currentItem() const = 0;
};
具体迭代器类:
template <typename T>
class ConcreteIterator : public Iterator<T> {
private:const std::vector<T>& collection;size_t currentIndex;public:ConcreteIterator(const std::vector<T>& collection): collection(collection), currentIndex(0) {}T first() override {return collection[0];}T next() override {currentIndex++;if (currentIndex < collection.size()) {return collection[currentIndex];}throw std::out_of_range("Iterator out of range");}bool isDone() const override {return currentIndex >= collection.size();}T currentItem() const override {if (currentIndex < collection.size()) {return collection[currentIndex];}throw std::out_of_range("Iterator out of range");}
};
聚合接口类:
template <typename T>
class Aggregate {
public:virtual ~Aggregate() {}virtual Iterator<T>* createIterator() const = 0;
};
具体聚合类:
template <typename T>
class ConcreteAggregate : public Aggregate<T> {
private:std::vector<T> items;public:ConcreteAggregate(std::initializer_list<T> elements) : items(elements) {}Iterator<T>* createIterator() const override {return new ConcreteIterator<T>(items);}size_t size() const {return items.size();}T getItem(size_t index) const {if (index < items.size()) {return items[index];}throw std::out_of_range("Index out of range");}
};
客户端代码:
#include <iostream>
#include <vector>
#include <stdexcept>int main() {ConcreteAggregate<int> aggregate({1, 2, 3, 4, 5});Iterator<int>* iterator = aggregate.createIterator();for (iterator->first(); !iterator->isDone(); iterator->next()) {std::cout << iterator->currentItem() << " ";}delete iterator;return 0;
}
运行结果:
1 2 3 4 5
为什么以模版形式实现迭代器而不使用多态性来实现迭代器
在 C++ 中,实现迭代器时选择使用模板而非多态性(即虚函数)主要有以下几个原因:
1. 性能考虑
模板是在编译时进行类型替换的,这意味着模板代码在编译时生成特定类型的代码实例,而不需要在运行时进行类型检查或虚函数调用。这带来了以下几个性能上的好处:
-
无虚函数开销:使用模板可以避免虚函数调用的开销。虚函数调用通常涉及通过虚函数表(vtable)间接访问函数,可能导致一些额外的性能开销。而模板直接生成目标类型的代码,函数调用可以内联,从而提高运行时性能。
-
优化机会:编译器在模板实例化时有更多的优化机会。由于编译器知道确切的类型,可以进行更多的优化,比如内联、消除不必要的操作等。
2. 灵活性和类型安全
-
强类型检查:模板提供了更强的类型安全性,因为所有类型信息在编译时就已经确定。模板迭代器在编译时会检查是否所有操作都符合类型约束,避免了在运行时发生类型错误。
-
支持更多容器:模板允许你为不同类型的集合(如
std::vector<T>
、std::list<T>
等)生成不同的迭代器实现,而不需要定义多个派生类。这样,模板迭代器可以更好地适应各种容器类型,而不需要为每种类型手动定义派生类。
3. 类型擦除的避免
- 避免类型擦除:在多态性实现中,为了支持不同类型的对象,通常需要使用指针或引用来指向基类。这涉及到类型擦除(type erasure),即在运行时丢失部分类型信息。而模板不会丢失类型信息,能够保留完整的类型信息,并在编译时确保类型的正确性。
4. 代码复用和泛型编程
-
代码复用:模板允许编写泛型代码,这样可以避免重复为不同类型编写类似的代码。通过使用模板,可以编写一次泛型迭代器,然后在不同类型的集合上复用该迭代器代码。
-
泛型编程的自然适应性:模板是一种强大的工具,尤其适用于泛型编程。在 C++ 标准库中,几乎所有容器的迭代器都是通过模板实现的,形成了一个一致的泛型编程体系(如 STL)。这使得模板迭代器可以无缝地与 C++ 标准库的算法(如
std::sort
,std::find
等)协同工作。
5. 减少内存开销
- 避免额外的对象分配:使用多态性时,通常需要动态分配内存以存储不同类型的对象,这会增加内存开销。模板迭代器由于不需要动态分配内存,可以在栈上分配,并且避免了额外的对象开销。
6. 模板元编程
- 支持高级编程技巧:模板不仅仅是类型安全和高效的,它还支持复杂的模板元编程技巧。这种能力允许在编译时执行计算和生成代码,从而进一步优化代码的执行效率。
什么时候选择多态性?
尽管模板具有很多优势,但在某些情况下,多态性(即使用虚函数)仍然是合适的选择:
-
需要在运行时处理异构集合:如果需要在运行时处理不同类型的对象(如一个集合中包含不同类型的对象),模板可能无法提供直接的支持,此时使用多态性可以更好地处理这些需求。
-
接口稳定性:如果需要提供一个稳定的接口供外部使用,而不希望外部依赖模板实现(模板通常定义在头文件中,容易暴露实现细节),则可以选择使用虚函数接口来隐藏实现。
总结
在 C++ 中,模板实现迭代器的方式由于性能、类型安全性、灵活性、和代码复用性等方面的优势,通常是优于使用多态性的。模板通过在编译时确定类型和生成代码,避免了运行时开销,使得迭代器更高效且灵活。但在处理需要运行时多态性或异构集合的情况下,多态性仍然是不可替代的。
优缺点
优点:
- 一致的遍历接口:迭代器模式为不同的聚合结构提供了一致的遍历接口,使得遍历操作与聚合对象的实现解耦。
- 封装性:迭代器模式可以避免暴露聚合对象的内部表示,保持了集合对象的封装性。
- 灵活性:可以为同一个聚合对象创建不同的迭代器,以支持不同的遍历方式(如正向、反向遍历)。
缺点:
- 可能增加复杂性:引入了额外的迭代器类,可能会增加系统的复杂性,特别是当需要支持多种迭代方式时。
- 对象数量增加:对于大型聚合对象,迭代器的创建和管理可能会导致较多的对象实例,增加内存开销。
应用场景
迭代器模式适用于以下场景:
- 需要遍历集合对象:如数组、链表、树形结构等,需要对集合对象中的元素进行遍历时。
- 需要多种遍历方式:如正序、倒序或特定条件下的遍历,可以为同一个聚合对象定义不同的迭代器。
- 隐藏聚合对象的实现细节:希望客户端在遍历集合时无需了解集合的具体实现(如内部数据结构)时。
总结
- 有些模式运用的技术机制可能会过时但是它的思想不会过时
- 迭代抽象:访问一个聚合对象的内容,而无需暴露它的内部表示。
- 迭代多态:在遍历不同的集合结构提供一个统一的接口。从而支持同样的算法在不同的集合结构上进行操作。
- 迭代器的健壮性考虑:便利的同时更改迭代器所在的集合结构会导致问题。
- 迭代器模式为集合对象提供了一种通用的遍历接口,使得客户端可以以一致的方式访问集合中的元素,而不需要了解集合的内部结构。通过封装遍历逻辑,迭代器模式有效地解耦了集合的实现和使用,但也可能引入额外的复杂性和开销。在需要对复杂集合进行多种遍历时,迭代器模式特别有用。