源码地址https://github.com/sadgeminids/IteratorLearning
为一个集合类提供遍历元素的功能,并且不暴露过多类的内部细节给使用者,尽量降低类编写者和使用者的耦合。
用通俗一点的话描述就是:想要对A遍历,A会给你一个B,然后B会告诉你怎么去遍历A。
UML图(扒自 图解设计模式):
1.接口Aggregate,实现该接口的类可遍历。接口最核心的方法是返回一个Iterator对象,该对象负责真正的遍历工作
2.需要遍历的集合类实现接口Aggregate
3.接口Iterator,提供遍历对象所需的接口
4.实现Iterator接口的类,针对具体的集合类完成遍历逻辑
再举个例子:有一个班(集合类),里面有很多学生(集合类里的元素)。老师想要遍历这个班所有对学生点名,但是他不知道怎么去遍历。这时候这个班给了老师一个点名手册(实现接口Iterator的类),然后老师就可以根据这个学生手册上记录的名字(Iterator提供的方法)去遍历学生。
这个例子中A就是班级,B是学生名单手册。
理解了设计模式核心,怎么去实现代码呢?不同的语言可能具体接口提供函数不太一样,但核心思想是一致的。这里用C#来实现,所以Iterator提供的接口可能会与上面的XML类图有一定出入。
1.接口Aggregate(IEnumerable)
很多语言标准库都提供了该接口,不用我们再单独去实现。比如C#中,这个接口叫IEnumerable,只要继承了这个接口,就表示你的类是可遍历的。
using UnityEngine;
using System.Collections;
// Please ignore box and unbox, this is just an example
public class Collectionable : IEnumerable{
private int[] m_iArray1 = new int[3]{1, 2, 3};
private int[] m_iArray2 = new int[3]{4, 5, 6};
}
2.实现继承IEnumerable接口里的方法
IEnumerable接口方法只有一个 -- public IEnumerator GetEnumerator()。这个IEnumerator又是什么东西呢?看看.Net库里的代码:
namespace System.Collections
{
[ComVisible (true), Guid ("496B0ABF-CDEE-11D3-88E8-00902754C43A")]
public interface IEnumerator
{
//
// Properties
//
object Current
{
get;
}
//
// Methods
//
bool MoveNext ();
void Reset ();
}
}
里面有2个接口和1个属性。Reset暂且不说,意思很明显。另外两个Current和MoveNext恰好就是我们遍历一个对象关键所在。其实看名字也能知道,我们遍历一个对象是不是刚好需要能够取当前值,并且移动到下一个?
看来要想遍历Collectionable类,必须还得再实现一个继承IEnumerator的类了:
public class CollectionEnumrator : IEnumerator{
private Collectionable m_collection;
private int m_index = -1;
public CollectionEnumrator(Collectionable collection){
m_collection = collection;
}
public System.Object Current{
get {
if(m_index < m_collection.m_iArray1.Length)
return m_collection.m_iArray1[m_index];
else if (m_index < m_collection.m_iArray1.Length + m_collection.m_iArray2.Length)
return m_collection.m_iArray2[m_index - m_collection.m_iArray1.Length];
else
return null;
}
}
public bool MoveNext(){
m_index += 1;
return m_index < m_collection.m_iArray1.Length + m_collection.m_iArray2.Length;
}
public void Reset(){
}
}
有了CollectionEnumrator对象,我们就可以实现public IEnumerator GetEnumerator()函数了:
public class Collectionable : IEnumerable{
private int[] m_iArray1 = new int[3]{1, 2, 3};
private int[] m_iArray2 = new int[3]{4, 5, 6};
public IEnumerator GetEnumerator(){
return new CollectionEnumrator (this);
}
}
上面代码可以了吗?当然不可以,因为这样是编译不过去的!
CollectionEnumrator 类保存了一个遍历对象Collectionable,要遍历对象必然也要访问对象。然而最大的问题是Collectionable的内部数据是private的!
可不可以把数据修改成public?可以,但是数据公开会暴露太多细节,这是类提供者不想看到的。而且公开了内部数据结构,何必再提供一个遍历对象呢,直接让使用者自己去访问就可以了,是不是?
Iterator设计模式出现本身就是为了解决类提供者和使用者耦合太强的问题,不想让暴露太多数据细节。可是我们要怎么解决无法访问private数据的问题呢?
很简答,把CollectionEnumrator放入Collectionable中,让它成为一个内部类就解决所有问题了:
using System.Collections;
// Please ignore box and unbox, this is just an example
public class Collectionable : IEnumerable{
private int[] m_iArray1 = new int[3]{1, 2, 3};
private int[] m_iArray2 = new int[3]{4, 5, 6};
public IEnumerator GetEnumerator(){
return new CollectionEnumrator (this);
}
public class CollectionEnumrator : IEnumerator{
private Collectionable m_collection;
private int m_index = -1;
public CollectionEnumrator(Collectionable collection){
m_collection = collection;
}
public System.Object Current{
get {
if(m_index < m_collection.m_iArray1.Length)
return m_collection.m_iArray1[m_index];
else if (m_index < m_collection.m_iArray1.Length + m_collection.m_iArray2.Length)
return m_collection.m_iArray2[m_index - m_collection.m_iArray1.Length];
else
return null;
}
}
public bool MoveNext(){
m_index += 1;
return m_index < m_collection.m_iArray1.Length + m_collection.m_iArray2.Length;
}
public void Reset(){
}
}
}
这样我们现在就实现来一个可遍历的集合类。
有个集合类,有了遍历类,怎么去调用接口遍历对象呢?
using System;
using System.Collections;
public class CollectionClient {
static void Main(string[] args) {
Collectionable collectAble = new Collectionable ();
IEnumerator iterator = collectAble.GetEnumerator ();
while (iterator.MoveNext()) {
Debug.LogError(iterator.Current);
}
}
}
先从集合类中取出IEnumerator对象,然后用MoveNext方法判断是否仍然有值,如果有可以用Current取出当前值。
感觉好像很麻烦啊,一点都不智能,能不能更简洁去调用这些接口呢?
当然,可以!为什么C#会设计这么一个接口,并且需要去实现这样几个方法呢,仅仅是为了规范吗?
当然,不是。C#会这么设计接口,是因为内部对于这种实现方式,提供了更为快捷的调用机制。
C#本身提供给我们一个关键字foreach,能够快捷实现遍历。先看如何使用foreach:
using System;
using System.Collections;
public class CollectionClient {
static void Main(string[] args) {
Collectionable collectAble = new Collectionable ();
foreach (int ele in collectAble)
Debug.LogError (ele);
}
}
对比上一种调用方式,代码简洁、方便了许多,一行代码就能搞定。而且foreach还能配合类型自动识别,更加方便:
foreach (var ele in collectAble)
Debug.LogError (ele);
从上面可以代码依稀可见,实现遍历的核心是IEnumerator对象,而IEnumerable所要做的仅仅只是去返回这么一个对象。那能不能不继承IEnumerable,绕过去,我们直接让集合类继承IEnumerator?试一试:
public class Collectiontor : IEnumerator {
private int[] m_iArray1 = new int[3]{1, 2, 3};
private int[] m_iArray2 = new int[3]{4, 5, 6};
private int m_index = -1;
public System.Object Current{
get {
if(m_index < m_iArray1.Length)
return m_iArray1[m_index];
else if (m_index < m_iArray1.Length + m_iArray2.Length)
return m_iArray2[m_index - m_iArray1.Length];
else
return null;
}
}
public bool MoveNext(){
m_index += 1;
return m_index < m_iArray1.Length + m_iArray2.Length;
}
public void Reset(){
}
}
有个编译错误:error CS1579: foreach statement cannot operate on variables of type `Collectiontor' because it does not contain a definition for `GetEnumerator' or is not accessible
大意就是说foreach不能作用在一个不包含 `GetEnumerator'方法的类型对象上面。我们把这个方法也实现一个:
public IEnumerator GetEnumerator(){
return this;
}
再用foreach调用,发现结果跟之前方式对比,一摸一样。调用方式也一样。我们是不是可以干掉 IEnumerable,感觉没什么卵用啊,直接让集合类继承IEnumerator效果没区别啊,还能省掉一个类的实现呢!
这里就要涉及到设计原则了,通常一个类只应该有一个职责,单一的职责使得类结构更清晰。同时,将遍历的职责拆分出来,单独实现,也在一定程度上使得对于集合类本身的修改不会影响遍历类。更重要的是,修改遍历类的实现不会触及到集合类的修改。比如我们可以实现从前往后,从后往前,各种各样的遍历类。每一个遍历方式灵活替换,不会触碰到原始的集合类任何代码。
上一个思考中出现的编译错误,已经在某种程度上提示我们,foreach是在内部涉及到 GetEnumerator 、Current 、MoveNext等方法了?
反编译一下代码看看(这两张图是在公司截的,所以类名有所不同,但是实现代码几乎一致,反编译的IL代码也是一样的):
红框里都是关键信息:
1.实例化集合类对象;
2.调用了GetEnumerator()方法;
3.调用get_current()方法,这个就是Current属性的get方法
4.调用MoveNext()方法,移动到下一个值
这就是C#本身对常用代码的优化,为我们提供了便捷的调用方式。
是的。每个自定义的集合类实现方式都不同,没有一种通用的Iterator可以适用于每一个集合类。不过,一般而言,集合类的编写者都会提供对应的Iterator实现类,所以使用者无需关注太多实现细节。
不仅是C#中提供的List,Dictionary等集合类实现了Iterator设计模式。C++中也有,只是名字不一样,就叫迭代器(iterator)。
#include
#include
using namespace std;
int main() {
vector ivec;
ivec.push_back(1);
ivec.push_back(2);
ivec.push_back(3);
ivec.push_back(4);
for(vector::iterator iter = ivec.begin();1. iter != ivec.end(); ++iter)
cout << *iter << endl;
}
c++中的迭代器也是根据每种类型都有单独对应的实现,确保跟集合类型契合。
foreach这么简单,那是不是可以肆无忌惮使用?
就如我一开始在代码中注释所言,这篇博客的代码是没有去关心装箱(box)和拆箱(unbox)的。仔细查看IEnumerator的方法,Current的返回类型是system.object,这是一个引用类型。可是我们集合类中的元素是值类型。
上面的IL截图中,也可以看到明确的 box int32 和 unbox.any int32。 装箱和拆箱肯定是有效率损耗的,所以,在性能无比珍贵的场景(比如手机游戏)中慎用foreach,至少引起装箱拆箱的要注意。如果,不考虑任何性能,怎么方便怎么来吧。