遍历器(Iterator)的作用是按照指定的顺序来访问一个集合中的所有元素,而不需要了解集合的详细数据结构。
1 概述
1.1 foreach语句
这种遍历方式对任何类型的数据都适用,因为所有数组都继承了.NET类库中定义的类System.Array,而该类继承了接口IEnmerable。在C#中,如果某个类型继承了接口IEnumerable,或者继承了泛型接口IEnumerable<T>,或者继承了泛型接口IEnumerable<T>的任何一个构造类型,那么称该类型是“可枚举的”。
可枚举类型通常表示一个集合。除数组之外,.NET类库中定义的枚举类型的例子有:字符串类String,表示XML文件节点的类XmlNode,表示动态列表的泛型类List<T>等。这些类型都可以使用foreach循环来遍历集合中的每一个元素。
1.2 Iterator模式
从很多事物中可以提取出许多共性的要素,而人们在设计过程中总是自觉不自觉地在使用这些要素,这些要素的抽象就是设计模式。
在各种各样的类库的设计中,面向对象的设计模式得到了广泛的应用。.NET类库的设计模式中使用到了Iterator模式,它是Erich Gamma总结的面向对象的设计模式这一,遍历器的概念正是对这一模式的实现。
Iterator模式的作用就是对集合中一系列元素进行访问,基本思想是:集合对象只负责维护集合中的各个元素,而对元素的访问则通过定义一个新的枚举器对象来进行;枚举器对象负责获取集合中的元素;并允许按照特定的顺序来遍历这些元素。与枚举器相关联的集合叫做可枚举集合。该模式的优点在于不需要暴露集合的内部结构,同时可以为一个集合实现多种遍历方式。
在.NET类库中,抽象可枚举集合所对应的类型可以是IEnumerable和IEnumerable<T>;如果一个非抽象类型继承了其中的任何一个接口,该类型就是一个具体的可枚举集合。抽象枚举器所对应的类型可以是IEnumerator和IEnumerator<T>,它们提供的方法成员用于查询可枚举集合的状态以及访问集合中的元素;而它们的非抽象派生类型同样都可以作为具体的枚举器。为集合获取枚举器对象的方法就是IEnumerable和IEnumerable<T>的GetEnumerator方法。
2 使用可枚举类型
2.1 IEnumerable和IEnumerable<T>接口
如果希望自己定义的集合对象也能够使用foreach循环,方法很简单,就是该类型继承接口IEnumerable或IEnumerable<T>。这两个接口都只定义了一个成员方法,其原型分别为:
IEnumerator GetEnumerator();
IEnumerator<T> GetEnumerator();
从这两个接口中派生的可枚举类型就都需要实现这两个接口方法。
public class Assemble<T> : IEnumerable { //字段 protected T[] m_list; ... //遍历器方法 public IEnumerator GetEnumerator() { for(int i=0;i<m_count;i++) yield return m_list[i]; } } }
通过GetEnumerator方法就得到了可枚举类型Assemble<T>的一个枚举器,通过它能够遍历集合m_list中的每个对象。该方法就是一个遍历器,而方法的执行代码就叫做遍历器代码。代码中使用的yield return语句用于依次生成一系列对象,其中的每一个都可以在foreach循环语句中被遍历,且遍历顺序与yield return语句生成的顺序一致。
yield return语句所生成对象的类型,必须与可枚举类型所管理的集合元素类型一致,或是可以隐式转换为这些元素的类型。如果可枚举类型继承的接口是IEnumerable,其元素类型就是object,这样yield return语句后面就可以是任何类型的表达式,但在foreach循环遍历时通常就要进行装箱和拆箱转换。而如果可枚举类型继承的是接口IEnumerable<T>或其某个构造类型,相应的元素类型就为T或其封闭类型。对于Assemble<T>这样的泛型类,更常见的情况是使其继承泛型接口IEnumerable<T>,这样就避免了类型转换的问题。
public class Assemble<T> : IEnumerable<T> { //字段 protected T[] m_list; ... //遍历器方法 public IEnumerator<T> GetEnumerator() { for(int i=0;i<m_count;i++) yield return m_list[i]; } } }
2.2 实现多种遍历方式
通过GetEnumerator方法所实现的遍历器叫做默认遍历器。C#还支持为同一个枚举类型实现多个遍历器,从而以不同的方式来遍历集合中的元素。非默认的遍历器也是通过方法成员来实现的,不同的是,它们要求方法成员的返回类型为IEnumerable或IEnumerable<T>。
public class Assemble<T> : IEnumerable<T> { //字段 protected T[] m_list; ... //遍历器方法 public IEnumerator<T> GetEnumerator() { for(int i=0;i<m_count;i++) yield return m_list[i]; } } public IEnumerable<T> GetReverseEnumerator() { for(int i=m_count-1;i>=0;i--) yield return m_list[i]; } }
这样就可以实现正向和逆向地输出集合的所有元素。
foreach语句中,关键字in后面的对象类型必须为可枚举类型。在可枚举类型定义代码中出现的this关键字所代表的对象显然也是可枚举的,因此可以将它作为非默认遍历器的返回值。例如,可以为泛型类Assemble<T>再增加下面的属性定义,而它所实现的遍历方式与默认遍历器相同:
public IEnumerable<T> ObverseEnumerator { get { return this; } }
下面是一个完整的示例:
class AssembleIteratorSample { static void Main() { Assemble<int> iAsm = new Assemble<int>(); iAsm.Add(4); iAsm.Add(12); iAsm.Add(9); iAsm.Add(1); iAsm.Add(8); Console.WriteLine("排序前:"); foreach (int i in iAsm) Console.WriteLine(i); Console.WriteLine("排序后:"); foreach (int i in iAsm.SortedEnumerator()) Console.WriteLine(i); } } /// <summary> /// 泛型类:集合Assemble /// </summary> public class Assemble<T> : IEnumerable<T> where T : IComparable<T> { //字段 protected T[] m_list; protected int m_count = 0; protected int m_capacity = 100; //属性 public int Length { get { return m_count; } } public int Capacity { get { return m_list.Length; } } //索引函数 public T this[int index] { get { return m_list[index]; } set { m_list[index] = value; } } //构造函数 public Assemble() { m_list = new T[m_capacity]; } public Assemble(int iCapacity) { m_capacity = iCapacity; m_list = new T[m_capacity]; } //方法 public void Add(T tp) { if (m_count < m_list.Length) m_list[m_count++] = tp; } public void Remove(T tp) { int index = this.Find(tp); if (index == -1) return; this.RemoveAt(index); } public void RemoveAt(int index) { for (int i = index; i < m_count - 1; i++) { m_list[i] = m_list[i + 1]; } m_list[--m_count] = default(T); } public int Find(T tp) { for (int i = 0; i < m_count; i++) if (m_list[i].Equals(tp)) return i; return -1; } public void Sort() { T tmp; for (int i = m_count - 1; i > 0; i--) { for (int j = 0; j < i; j++) { if (this[j].CompareTo(this[j + 1]) > 0) { tmp = this[j + 1]; this[j + 1] = this[j]; this[j] = tmp; } } } } public IEnumerator<T> GetEnumerator() { for(int i = 0; i < m_count; i++) yield return m_list[i]; } //要添加对System.Collections IEnumerator IEnumerable.GetEnumerator() { return this.GetEnumerator(); } public IEnumerable<T> SortedEnumerator() { int[] index = new int[m_count]; for (int i = 0; i < m_count; i++) index[i] = i; int iTmp; for (int i = m_count - 1; i > 0; i--) { for (int j = 0; j < i; j++) { if (m_list[index[j]].CompareTo(m_list[index[j + 1]]) > 0) { iTmp = index[j + 1]; index[j + 1] = index[j]; index[j] = iTmp; } } } for (int i = 0; i < m_count; i++) yield return m_list[index[i]]; } }
程序输出结果:
排序前: 4 12 9 1 8 排序后: 1 4 8 9 12 请按任意键继续. . .
2.3 带参遍历
实现非默认遍历器方法还可以带有参数,通过传递参数来控制遍历的过程。
class SpecialIteratorSample { static void Main() { Assemble<int> iAsm = new Assemble<int>(10); for (int i = 1; i <= 10; i++) iAsm.Add(i); foreach (int x in iAsm) { foreach (int y in iAsm) { Console.Write("{0,3}", x * y); } Console.WriteLine(); } } } /// <summary> /// 泛型类:集合Assemble /// </summary> public class Assemble<T> : IEnumerable<T> { //字段 protected T[] m_list; protected int m_count = 0; protected int m_capacity = 100; //属性 public int Length { get { return m_count; } } public int Capacity { get { return m_list.Length; } } //索引函数 public T this[int index] { get { return m_list[index]; } set { m_list[index] = value; } } //构造函数 public Assemble() { m_list = new T[m_capacity]; } public Assemble(int iCapacity) { m_capacity = iCapacity; m_list = new T[m_capacity]; } //方法 public void Add(T tp) { if (m_count < m_list.Length) m_list[m_count++] = tp; } public void Remove(T tp) { int index = this.Find(tp); if (index == -1) return; this.RemoveAt(index); } public void RemoveAt(int index) { for (int i = index; i < m_count - 1; i++) { m_list[i] = m_list[i + 1]; } m_list[--m_count] = default(T); } public int Find(T tp) { for (int i = 0; i < m_count; i++) if (m_list[i].Equals(tp)) return i; return -1; } public IEnumerator<T> GetEnumerator() { return SpecialEnumerator(0,m_count-1,1,false).GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return this.GetEnumerator(); } public IEnumerable<T> ReverseEnumerator() { return SpecialEnumerator(0, m_count - 1, 1, true); } public IEnumerable<T> RangeEnumerator(int iStart, int iEnd, bool bReverse) { return SpecialEnumerator(iStart, iEnd, 1, bReverse); } public IEnumerable<T> SpecialEnumerator(int iStart, int iEnd, int iPace, bool bReverse) { if (!bReverse) { for (int i = iStart; i < iEnd; i += iPace) yield return m_list[i]; } else { for (int i = iEnd; i >= iStart; i -= iPace) yield return m_list[i]; } } }
程序输出结果:
1 2 3 4 5 6 7 8 9 2 4 6 8 10 12 14 16 18 3 6 9 12 15 18 21 24 27 4 8 12 16 20 24 28 32 36 5 10 15 20 25 30 35 40 45 6 12 18 24 30 36 42 48 54 7 14 21 28 35 42 49 56 63 8 16 24 32 40 48 56 64 72 9 18 27 36 45 54 63 72 81 请按任意键继续. . .
3 使用枚举器
如果某个类型继承了接口IEnumerator和IEnumerator<T>(包括其任何构造类型),它就是一个合法的枚举器,它需要实现的接口成员包括Current属性和MoveNext方法。继承接口IEnumerator的枚举器对象只能与继承接口IEnumerable的可枚举对象相关联;而继承接口IEnumerator<T>(或其构造类型)的枚举器对象只能与继承接口IEnumerable<T>(或其构造类型)的可枚举对象相关联,并且二者应以相同的封闭类型来替换类型参数T。
接口IEnumerator和IEnumerator<T>的Current属性表示可枚举类型中当前被访问的元素,它只有get访问函数,且返回类型与元素类型相同。即对于继承接口IEnumerator的枚举器,返回类型为object;对于继承接口IEnmerator<T>(或其构造类型)的枚举器,返回类型为T(或相应的封闭类型)。
MoveNext方法表示访问可枚举类型中的下一下元素。该方法没有参数,返回类型为布尔类型。如果当前被访问的元素已经是最后一个被遍历的元素,那么MoveNext方法简单地返回布尔值false;否则Current属性被修改为下一个元素,并返回布尔值true。
此外,由于接口IEnumerator<T>还继承了接口IDisposable,所以继承接口IEnumerator<T>(或其构造类型)的枚举器还必须实现接口IDisposable的Dispose方法。该方法中的代码通常用于释放资源。
接下来的程序用于斐波那契数列的计算与输出。斐波那契数列F(n)的计算公式为:
F(1) = 1 F(2) = 2 F(n) = F(n-1) + F(n-2),n>2
程序中定义了一个枚举器和一个可枚举类型。枚举器SequenceFBIterator始终保持两个整数字段,并在MoveNext方法中通过这两个字段来计算下一个数列元素的值。可枚举类型FBSequence中则维护了一个SequenceFBIterator类型的枚举器对象,并通过调用该对象的属性和方法来实现数列的遍历。
尽管数列的计算都是在枚举代码中进行的,但本例程序的主方法中并没有涉及到任何枚举器对象,只是通过可枚举类型FBSequence提供的遍历器就完成了所需要的功能:
class FindSample { static void Main() { Console.WriteLine("请输入数列的长度:"); int iLen; while (!int.TryParse(Console.ReadLine(), out iLen) || iLen < 1 || iLen > 50) { Console.WriteLine("请输入1~50之间的一个整数:"); } FBSequence seq = new FBSequence(); int count = 1; foreach (long i in seq.GetEnumerator(iLen)) Console.WriteLine("F({0}) = {1}", count++, i); } } /// <summary> /// 枚举器 /// </summary> public class SequenceFBIterator { //字段 private long m_value0; private long m_value1; //属性 public long Current { get { return m_value0; } } //构造函数 public SequenceFBIterator() { m_value0 = 1; m_value1 = 2; } //方法 public bool MoveNext() { long iTmp = m_value1; m_value1 = m_value1 + m_value0; m_value0 = iTmp; return true; } public void Dispose() { } } /// <summary> /// 可枚举类型 /// </summary> public class FBSequence:IEnumerable<long> { //字段 private SequenceFBIterator m_iterator; //构造函数 public FBSequence() { m_iterator = new SequenceFBIterator(); } //方法 public IEnumerable<long> GetEnumerator(int iLen) { for (int i = 0; i < iLen; i++) { yield return m_iterator.Current; m_iterator.MoveNext(); } } public IEnumerator<long> GetEnumerator() { return GetEnumerator(10).GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return this.GetEnumerator(); } }
输出结果:
请输入数列的长度: 20 F(1) = 1 F(2) = 2 F(3) = 3 F(4) = 5 F(5) = 8 F(6) = 13 F(7) = 21 F(8) = 34 F(9) = 55 F(10) = 89 F(11) = 144 F(12) = 233 F(13) = 377 F(14) = 610 F(15) = 987 F(16) = 1597 F(17) = 2584 F(18) = 4181 F(19) = 6765 F(20) = 10946 请按任意键继续. . .
4 遍历器工作机制
4.1 遍历器代码
对可枚举类型和枚举器有了一个深入的了解之后,现在就可以给C#语言中对遍历器的准确定义了。定义的内容包括:
• 遍历器是可枚举类型的一个方法成员,该成员可以是方法、属性的get访问函数,以及重载的操作符;
• 遍历器的返回类型应为接口IEnumerable、IEnumerable<T>、IEnumerator、IEnumerator<T>中的一个(对泛型接口,包括其所有构造类型);
• 遍历器的执行代码中应出现yield return语句或yield break语句。
yield return和yield break语句都是C#的遍历器代码中所特有的语句。yield return语句用于生成一系列被遍历的对象,而yield break语句则用于停止遍历。例如下面的遍历器所实现的遍历为“空”遍历。不访问任何元素。
public IEnumerator<T> GetEnumerator() { yield break; }
下面两个遍历器,第一个会忽略所有的空对象,第二个遇到空对象就停止遍历:
public IEnumerable<T> SkippingEnumerator() { for(int i=0;i<m_count;i++) { if(m_list[i]!=null) yield return m_list[i]; } } public IEnumerable<T> BreakingEnumerator() { for(int i=0;i<m_count;i++) { if(m_list[i]==null) yield break; yield return m_list[i]; } }
遍历器代码的使用存在下列一些限制:
• 如果某个方法成员实现的是一个遍历器,那么该方法成员不能使用任何引用参数或输出参数;
• 遍历器代码中不能出现单独的return语句。
下面的代码是不合法的,因为在遍历器代码中使用了return语句:
public IEnumerable<T> BreakingEnumerator() { for(int i=0;i<m_count;i++) { if(m_list[i]==null) return null; yield return m_list[i]; } }
而下面的代码则是合法的,因为方法的执行代码中并没有出现yield return语句或yield break语句,因此该方法并不是一个遍历器,它可以像普通方法那样使用return语句:
public IEnumerable<T> SkippingEnumerator() { for(int i=0;i<m_count;i++) { if(m_list[i]==null) return null; } }
4.2 遍历流程
首先看看集合类Assemble中默认的遍历器
public IEnumerator<T> GetEnumerator() { for(int i = 0; i < m_count; i++) yield return m_list[i]; }
执行到下面的foreach循环语句时,程序就会使用到该遍历器:
foreach(int i in iAsm) Console.WriteLine(i);
注意此时并不是立即执行遍历器代码,而是首先根据遍历器的参数和返回类型创建一个临时的枚举对象。此例中GetEnumerator方法无参数,返回类型为IEnumerator<int>,所以枚举器对象的类型为Enumerator<int>。该对象继承了接口IEnumerator<int>,并实现了该接口的属性Current和方法MoveNext。
枚举器对象可以有4种状态:准备、运行、挂起和完成。枚举器对象被创建之后,它处于准备状态。
当foreach语句开始第一次循环时,枚举器对象的MoveNext方法被被调用,其状态也由准备转换到运行状态,此时遍历器代码被调用;执行到遍历器代码中的yield return语句时,该语句所生成的对象被赋值给枚举器对象的Current属性,其值就是正在被遍历的元素。之后枚举器对象的状态变为挂起。
此后每循环一次,枚举器对象的MoveNext方法就被调用一次,枚举器对象的状态由挂起转换到运行,执行遍历器代码,遇到yield return语句时给Current属性赋值,状态再变为挂起......,直到遍历到最后一个元素,此时再调用MoveNext方法将返回布尔值false,于是枚举器对象的状态将变为完成,循环结束,程序控制权返回。
而如果在执行遍历器代码时遇到yield break语句,枚举器对象将立即转换到完成状态,并返回程序控制权。
因此上面的foreach循环语句等效下下面代码:
IEnumerator<int> iterator = iAsm.GetEnumerator(); while (iterator.MoveNext()) Console.WriteLine(iterator.Current);
集合类Assemble中的另一个遍历器public IEnumerable<T> SortedEnumerator(),实现遍历的代码仍然是最后两行,因些对于调用代码:
foreach (int i in iAsm.SortedEnumerator()) Console.WriteLine(i);
其等效代码为:
IEnumerable<int> enum1 = iAsm.SortedEnumerator(); IEnumerator<int> iterator1 = enum1.GetEnumerator(); while (iterator1.MoveNext()) Console.WriteLine(iterator1.Current);
5 示例程序:联系人分类输出
程序定义了一个泛型类IndexedAssemble<T>,它从继承了类Assemble<T>,并通过增加一个索引数组来提高排序的性能。最关键的一个方法是ClassifyEnumerator,它提供了针对集合中不同类型的元素进行遍历的功能。它通过object类型的GetType方法获得对象的确切类型,并与指定的元素类型进行比较,二者相等时方调用yield return语句。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using P13_2; using P13_3; using P14_2; namespace P14_4 { class TypeIteratorSample { static void Main() { IndexedAssemble<Contact> conAsm = new IndexedAssemble<Contact>(10); conAsm.Add(new Business("Mike Even")); conAsm.Add(new Classmate("李明")); conAsm.Add(new Contact("David Gries")); conAsm.Add(new Business("张鹏")); conAsm.Add(new Business("White Brown")); conAsm.Add(new Classmate("Frank Douf")); conAsm.Add(new Classmate("何子杰")); ConsoleRW rw1 = new ConsoleRW(); string sType = rw1.Read("请选择输出联系人的类型(1.所有 2.商务 3.同学)"); IEnumerable<Contact> enums; if (sType == "2") enums = conAsm.ClassifyEnumerator("P13_3.Business", true, false); else if (sType == "3") enums = conAsm.ClassifyEnumerator("P13_3.Classmate", true, false); else enums = conAsm.SortedEnumerator(false); foreach (Contact c1 in enums) { c1.Output(rw1); Console.WriteLine(); } } } } /// <summary> /// 泛型类:索引集合 /// </summary> public class IndexedAssemble<T> : Assemble<T>,IEnumerable<T> where T : IComparable<T> { //字段 protected int[] m_indexs; //构造函数 public IndexedAssemble() : base() { m_indexs = new int[m_capacity]; } public IndexedAssemble(int iCapacity) : base(iCapacity) { m_indexs = new int[m_capacity]; } //方法 public void Sort() { for (int i = 0; i < m_count; i++) m_indexs[i] = i; int iTmp; for (int i = m_count - 1; i > 0; i--) { for (int j = 0; j < i; j++) { if (m_list[m_indexs[j]].CompareTo(m_list[m_indexs[j + 1]]) > 0) { iTmp = m_indexs[j + 1]; m_indexs[j + 1] = m_indexs[j]; m_indexs[j] = iTmp; } } } } public IEnumerable<T> SortedEnumerator(bool bReverse) { Sort(); if (!bReverse) { for (int i = 0; i < m_count; i++) yield return m_list[m_indexs[i]]; } else { for (int i = m_count - 1; i >= 0; i--) yield return m_list[m_indexs[i]]; } } public IEnumerable<T> ClassifyEnumerator(string sType, bool bSort, bool bReverse) { if (!bReverse) { for (int i = 0; i < m_count; i++) { if (bSort) { Sort(); if (m_list[m_indexs[i]].GetType().ToString() == sType) yield return m_list[m_indexs[i]]; } else { if (m_list[i].GetType().ToString() == sType) yield return m_list[i]; } } } else { for (int i = m_count - 1; i >= 0; i--) { if (bSort) { Sort(); if (m_list[m_indexs[i]].GetType().ToString() == sType) yield return m_list[m_indexs[i]]; } else { if (m_list[i].GetType().ToString() == sType) yield return m_list[i]; } } } } }
程序输出结果:
请输入请选择输出联系人的类型(1.所有 2.商务 3.同学) 2 姓名:Mike Even 性别:女 公司: 职务: 办公电话: 商务电话: 住宅电话: 手机: 姓名:White Brown 性别:女 公司: 职务: 办公电话: 商务电话: 住宅电话: 手机: 姓名:张鹏 性别:女 公司: 职务: 办公电话: 商务电话: 住宅电话: 手机: 请按任意键继续. . .
6 小结
可枚举类型通常用于管理一组元素的集合,遍历器是可枚举类型的特殊方法成员,使用它可以按指定的方式来遍历集合中的全部或部分元素,并使foreach循环能够作用于可枚举的对象。
遍历器代码在执行过程中会创建一个枚举对象,并由该对象负责执行遍历的流程。开发人员可以在程序中创建自己的枚举器类型。