.Net中的设计模式——Iterator模式
一、模式概述
在面向对象设计时,我们常常需要辨认对象的职责。理想的状态下,我们希望自己建立的对象只具有一个职责。对象的责任越少,则该对象的稳定性就越好,受到的约束也就越少。职责分离,可以最大限度地减少彼此之间的耦合程度,从而建立一个松散耦合的对象网络。
职责分离的要点是对被分离的职责进行封装,并以抽象的方式建立起彼此之间的关系。在C#中,我们往往将这些可能变化的对象抽象为接口和抽象类,从而将原来的具体依赖改变为抽象依赖。对象不再受制于具体的实现细节,这就代表他们是可被替换的。
要在设计上做到这一点,首先就要学会分辨职责,学会分辨哪些职责是对象中可变的。以集合对象为例,集合是一个管理和组织数据对象的数据结构。这就表明集合首先应具备一个基本属性,就是集合能够存储数据。这其中包含存储数据的类型、存储空间的大小、存储空间的分配、以及存储的方式和顺序。不具备这些特点,则该对象就不成其为集合对象。也就是说,上述这些属性是集合对象与身俱来的,是其密不可分的职责。然而,集合对象除了能够存储数据外,还必须提供访问其内部数据的行为方式,这是一种遍历机制。同时这种遍历方式,或会根据不同的情形提供不同的实现,如顺序遍历,逆序遍历,或是二叉树结构的中序、前序、后序遍历。
现在我们已经分辨出集合对象拥有的两个职责:一是存储内部数据;二是遍历内部数据。从依赖性来看,前者为集合对象的根本属性,属于一生俱生,一亡俱亡的关系;而后者既是可变化的,又是可分离的。因此,我们将遍历行为分离出来,抽象为一个迭代器,专门提供遍历集合内部数据对象的行为。这就是Iterator模式的本质。
如一个列表对象List,它提供了遍历列表各元素的能力,这种遍历的行为可能包含两种:顺序和逆序遍历。对于一般的List对象而言,采用顺序遍历的方式;而对于特定的List对象,如ReverseList,则按照逆序遍历的方式访问其内部数据。如果不将存储数据和访问数据的职责分离,为实现ReverseList类,就需要重写其父类List中所有用于遍历数据的方法。现在我们采用Iterator模式,在List对象中分离出迭代器IListIterator,那就只需要为这个继承自List对象的ReverseList对象,提供逆序迭代器ReverseListIterator就可以了,如下面的类图所示:
代码如下:
public interface IListIterator
{
void First();
void Last();
bool MoveNext();
}
public class SequenceListIterator:IListIterator
{
private List list = null;
private int index;
public SequenceListIterator(List list)
{
index = -1;
this.list = list;
}
public void First()
{
index = 0;
}
public void Last()
{
index = list.Count;
}
public bool MoveNext()
{
index ++;
return list.Count > index;
}
}
public class ReverseListIterator:IListIterator
{
private List list = null;
private int index;
public ReverseListIterator(List list)
{
index = list.Count;
this.list = list;
}
public void First()
{
index = list.Count - 1;
}
public void Last()
{
index = 0;
}
public bool MoveNext()
{
index --;
return index >= 0;
}
}
public class List
{
public virtual IListIterator CreateIterator()
{
return new SequenceListIterator(this);
}
}
public class ReverseList:List
{
public override IListIterator CreateIterator()
{
return new ReverseListIterator(this);
}
}
我们看到,List类通过方法CreateIterator(),创建了SequenceListIterator对象,使得List集合对象能够用顺序迭代的方式遍历内部元素。而要使ReverseList采用逆序的方式遍历其内部元素,则只需重写父类List的CreateIterator()方法,通过创建ReverseListIterator对象,来建立集合与具体迭代器之间的关系。
二、 .Net中的Iterator模式
在.Net中,IEnumerator接口就扮演了Iterator模式中迭代器的角色。IEnumerator的定义如下:
public interface IEnumerator
{
bool MoveNext();
Object Current {get; }
void Reset();
}
该接口提供了遍历集合元素的方法,其中主要的方法是MoveNext()。它将集合中的元素下标移到下一个元素。如果集合中没有元素,或已经移到了最后一个,则返回false。能够提供元素遍历的集合对象,在.Net中都实现了IEnumerator接口。
我们在使用.Net集合对象时,会发现一个方法GetEnumerator()方法,它类似前面提到的List对象的CreateIterator()方法。该方法返回的类型是IEnumerator,其内部则是创建并返回一个具体的迭代器对象。正是通过这个方法,建立了具体的集合对象与迭代器之间的关系。
与通常的Iterator模式实现不同,.Net Framework将GetEnumerator()方法单独抽象出来,定义了接口IEnumerable:
public interface IEnumerable
{
IEnumerator GetEnumerator();
}
IEnumerable接口就像迭代功能的标识,如果集合对象需要具备迭代遍历的功能,就必须实现该接口,并在具体实现中,创建与自身有关系的具体迭代器对象。而要获得该集合对象对应的迭代器,就可以通过该方法,如:
ArrayList al = new ArrayList();
IEnumerator iterator = al.GetEnumerator();
下面,我就以ArrayList对象为例,讨论一下.Net中Iterator模式的实现方式。首先,我们来看看ArrayList类的定义:
public class ArrayList : IList, ICloneable
它分别实现了IList和ICloneable接口。其中,ICloneable接口提供了Clone方法,与本文讨论的内容无关,略过不提。IList接口则提供了和链表相关的操作,如Add,Remove等。这也是ArrayList和Array的不同之处。而IList接口又实现了ICollection接口。
public interface IList : ICollection
ICollection接口是所有集合类型的公共接口,它提供了获得集合长度和同步处理的一些方法,不过在这里,我们需要注意的是它实现了IEnumerable接口:
public interface ICollection : IEnumerable
追本溯源,ArrayList类型间接地实现了IEnumerable接口。在ArrayList中,IEnumerable接口的GetEnumerator()方法实现代码如下:
public virtual IEnumerator GetEnumerator() {
return new ArrayListEnumeratorSimple(this);
}
GetEnumerator()方法是一个虚方法,这说明,我们可以自定义一个集合类型继承ArrayList,重写这个方法,创建和返回不同的IEnumerator对象,从而实现不同的遍历方式。
为了实现ArrayList的遍历功能,采用的Iterator模式结构如下图所示:
其中,类ArrayListEnumeratorSimple的实现如下所示:
[Serializable]
private class ArrayListEnumeratorSimple : IEnumerator, ICloneable
{
// Methods
internal ArrayListEnumeratorSimple(ArrayList list)
{
this.list = list;
this.index = -1;
this.version = list._version;
this.currentElement = list;
}
public object Clone(){//实现略}
public virtual bool MoveNext()
{
if (this.version != this.list._version)
{
throw new InvalidOperationException(Environment.GetResourceString("InvalidOperation_EnumFailedVersion"));
}
if (this.index < (this.list.Count - 1))
{
this.index++;
this.currentElement = this.list[this.index];
return true;
}
this.currentElement = this.list;
this.index = this.list.Count;
return false;
}
public virtual void Reset()
{
if (this.version != this.list._version)
{
throw new InvalidOperationException(Environment.GetResourceString("InvalidOperation_EnumFailedVersion"));
}
this.currentElement = this.list;
this.index = -1;
}
// Properties
public virtual object Current
{
get
{
object obj1 = this.currentElement;
if (obj1 != this.list)
{
return obj1;
}
if (this.index == -1)
{
throw new InvalidOperationException(Environment.GetResourceString("InvalidOperation_EnumNotStarted"));
}
throw new InvalidOperationException(Environment.GetResourceString("InvalidOperation_EnumEnded"));
}
}
// Fields
private object currentElement;
private int index;
private ArrayList list;
private int version;
}
ArrayListEnumeratorSimple实现了IEnumerator接口,实现了MoveNext()、Current、Reset()方法或属性。该类是一个私有类型,其构造函数则被internal修饰符限制。在自定义的构造函数中,传入的参数类型是ArrayList。正是通过构造函数传递需要遍历的ArrayList对象,来完成MoveNext()、Current、Reset()等操作。
下面,我们来看看如何通过IEnumerator来实现对ArrayList的遍历操作。
using System;
using System.Collections;
using NUnit.Framework
[TestFixture]
public class Tester
{
[Test]
public void TestArrayList()
{
ArrayList al = new ArrayList();
al.Add(5);
al.Add(“Test”);
IEnumerator e = al.GetEnumerator();
e.MoveNext();
Assert.AreEqual(5,e.Current);
e.MoveNext();
Assert.AreEqual(“Test”,e.Current);
}
}
而要遍历ArrayList内部所有元素,方法也很简单:
while (e.MoveNext())
{
Console.WriteLine(e.Current.ToString());
}
事实上,为了用户更方便地遍历集合对象的所有元素,在C#中提供了foreach语句。该语句的实质正是通过IEnumerator的MoveNext()方法来完成遍历的。下面的语句与刚才那段代码是等价的:
foreach (object o in al)
{
Console.WriteLine(o.ToString());
}
为了验证foreach语句与迭代器的关系,我们来自定义一个ReverseArrayList类。要求遍历这个类的内部元素时,访问顺序是逆序的。要定义这样的一个类,很简单,只需要继承ArrayList类,并重写GetEnumerator()方法既可。
public class ReverseArrayList:ArrayList
{
public override IEnumerator GetEnumerator()
{
return new ReverseArrayListEnumerator(this);
}
}
其中,类ReverseArrayListEnumerator,实现了接口IEnumerator,它提供了逆序遍历的迭代器:
public class ReverseArrayListEnumerator:IEnumerator
{
public ReverseArrayListEnumerator(ArrayList list)
{
this.list = list;
this.index = list.Count;
this.currentElement = list;
}
#region IEnumerator Members
public virtual void Reset()
{
this.currentElement = this.list;
this.index = this.list.Count;
}
public virtual object Current
{
get
{
object obj1 = this.currentElement;
if (obj1 != this.list)
{
return obj1;
}
if (this.index == -1)
{
throw new InvalidOperationException("Out of the Collection");
}
throw new InvalidOperationException("Out of the Collection");
}
}
public virtual bool MoveNext()
{
if (this.index > 0)
{
this.index--;
this.currentElement = this.list[this.index];
return true;
}
this.currentElement = this.list;
this.index = 0;
return false;
}
#endregion
private object currentElement;
private int index;
private ArrayList list;
}
注意ReverseArrayListEnumerator类与前面的ArrayListEnumeratorSimple类的区别,主要在于遍历下一个元素的顺序。ReverseArrayListEnumerator中的MoveNext()方法,将下标往前移动,以保证元素遍历的逆序。同时在构造函数初始化时,将整个ArrayList对象的元素个数赋予下标的初始值:
this.index = list.Count;
我们来比较一下ArrayList和ReversieArrayList类之间,通过foreach遍历后的结果。
[STAThread]
public static void Main(string[] args)
{
ArrayList al = new ArrayList();
al.Add(1);
al.Add(2);
al.Add(3);
ReverseArrayList ral = new ReverseArrayList();
ral.Add(1);
ral.Add(2);
ral.Add(3);
Console.WriteLine("The Sequence ArrayList:");
foreach (int i in al)
{
Console.Write("{0} ",i);
}
Console.WriteLine();
Console.WriteLine("The Reverse ArrayList:");
foreach (int i in ral)
{
Console.Write("{0} ",i);
}
Console.ReadLine();
}
我们分别将数字1,2,3以同样的顺序添加到ArrayList和ReverseArrayList对象中,然后再通过foreach语句遍历输出其内部元素。运行后,很明显可以看到遍历ArrayList对象al,其顺序为1,2,3;而ReverseArrayList则为3,2,1。
由于我们应用Iterator模式,将迭代器与集合对象完全分离,所以,即便我们完全修改了ReverseArrayList的遍历方式,实现ReverseArrayList也是非常容易的,同时它并没有影响到集合对象本身存储数据对象的职能。