Part1 迭代器模式 与 接口
IEnumerable
IEnumerator
interface IEnumerable
{
IEnumerator GetEnumerator();
}
// 泛型版本 : IEnumerator<T>
interface IEnumerator
{
object Current { get; }
bool MoveNext();
void Reset();
}
这两个接口用于实现 迭代器 这种设计模式。
迭代器模式:
在软件构建过程中,集合对象内部结构常常变化各异。但对于这些集合对象,我们希望在不暴露其内部结构的同时,可以让外部客户代码透明地访问其中包含的元素;同时这种“透明遍历”也为“同一种算法在多种集合对象上进行操作”提供了可能。使用面向对象技术将这种遍历机制抽象为“迭代器对象”为“应对变化中的集合对象”提供了一种优雅的方式。
迭代器模式是一种行为设计模式,简单而言,就是将对集合的遍历有“外部控制”变为“内部控制”,将其封装起来。
数组就是将遍历完全交由外部处理。
Iterator模式的几个要点
- 迭代抽象:访问一个聚合对象的内容而无需暴露它的内部表示。
- 迭代多态:为遍历不同的集合结构提供一个统一的接口,从而支持同样的算法在不同的集合结构上进行操作。
- 迭代器的健壮性考虑:遍历的同时更改迭代器所在的集合结构,会导致问题。(所以 C# 中在 foreach 操作时,不允许更改集合,如果外部有更改,则会报错)。
Part2 foreach 语句的等价形式(while循环)
foreach(var p in Persons)
{
WriteLine(p);
}
// 等价于一个 while 循环
IEnumerator<Person> enumerator = persons.GetEnumerator();
while(enumerator.MoveNext())
{
Person p = enumerator.Current;
WriteLine(p);
}
- 可以看到,这里并没有调用
Reset
方法,此方法通常用于与 COM 的交互操作,许多 .NET 枚举器抛出 NotSupportedException; - 集合可以被 foreach, 不一定需要实现 IEnumerable 接口,有 GetEnumerator 方法即可。
- 一个集合类可以提供多个不同的 GetEnumerator 实现,如 GetEnumerator1,GetEnumerator2,返回不同的 IEnumerator,以实现不同的迭代功能。(见下文)
Part3 IEnumerator 与 yield
一个集合类想要支持被迭代,最主要的是构造一个 Enumerator 类,实现 IEnumerator 接口,在 GetEnumerator 方法中返回这个 Enumerator 类。
如此,在 Enumerator 类中,需要维护 Current 属性和 MoveNext 方法,在 MoveNext 方法中,更新 Current 的值,并返回是否还有后续值的 bool 判断。
在实现 IEnumerator 接口时,通常也要实现其泛型版本 IEnumerator
。
这段文字看起来有点晕,实际上,实现一个 IEnumerator 也是一个苦力活。在实际的编程中,一般直接使用已有的集合元素,不必从头实现一个 IEnumerator 。
yield
是 C# 提供语法糖,可以方便的实现 IEnumerator 接口。如:
public IEnumerator<string> GetEnumerator()
{
yield return "A";
yield return "B";
yield return "C";
// ...
yield return "Z";
}
这样,实际上就实现了一个集合,这个集合保存了大写的26个字母。
yield return
语句返回集合的一个元素,并移动到下一个元素,相当于同时维护 Current
和 MoveNext
;yield break
可停止迭代。
使用 yield
,编译器会创建一个状态机,用于实际维护 Current
和 MoveNext
。
Part4 实现多个不同的 IEnumerator
有一个 MusicCollection 集合类,里面包含了 IList
public class MusicCollection
{
private IList<Music> MusicList;
public MusicCollection(IList<Music> musicList)
{
MusicList = musicList;
}
public IEnumerator<string> GetTitleEnumerator()
{
for(int i=0;i<MusicList.Length;i++)
{
yield return MusicList[i].Title;
}
}
public IEnumerator<string> GetAuthorEnumerator()
{
for(int i=0;i<MusicList.Length;i++)
{
yield return MusicList[i].Author;
}
}
public IEnumerator<string> GetTimeEnumerator()
{
for(int i=0;i<MusicList.Length;i++)
{
yield return MusicList[i].Time;
}
}
}
// 外部调用:
pubic class Test
{
public void Test()
{
var musicList = new List<Music>();
var musicCollection = new MusicCollection(musicList);
foreach(string title in musicCollection.GetTitleEnumerator())
{
Console.WriteLine(title);
}
}
}
迭代器中还可以返回迭代器(嵌套),有趣的用法。
Part5 线程安全
迭代显然是非线程安全的,每次IEnumerable都会生成新的IEnumerator,从而形成多个互相不影响的迭代过程。
在迭代过程中,不能修改迭代集合,否则不安全。
参考资料