迭代器模式 与 C# IEnumerator/IEnumerable

Part1 迭代器模式 与 接口

IEnumerable
IEnumerator

1
2
3
4
5
6
7
8
9
10
11
12
interface IEnumerable
{
IEnumerator GetEnumerator();
}
// 泛型版本 : IEnumerator<T>
interface IEnumerator
{
object Current { get; }
bool MoveNext();
void Reset();
}

这两个接口用于实现 迭代器 这种设计模式。

迭代器模式:
在软件构建过程中,集合对象内部结构常常变化各异。但对于这些集合对象,我们希望在不暴露其内部结构的同时,可以让外部客户代码透明地访问其中包含的元素;同时这种“透明遍历”也为“同一种算法在多种集合对象上进行操作”提供了可能。使用面向对象技术将这种遍历机制抽象为“迭代器对象”为“应对变化中的集合对象”提供了一种优雅的方式。

迭代器模式是一种行为设计模式,简单而言,就是将对集合的遍历有“外部控制”变为“内部控制”,将其封装起来。

数组就是将遍历完全交由外部处理。

Iterator模式的几个要点

  • 迭代抽象:访问一个聚合对象的内容而无需暴露它的内部表示。
  • 迭代多态:为遍历不同的集合结构提供一个统一的接口,从而支持同样的算法在不同的集合结构上进行操作。
  • 迭代器的健壮性考虑:遍历的同时更改迭代器所在的集合结构,会导致问题。(所以 C# 中在 foreach 操作时,不允许更改集合,如果外部有更改,则会报错)。

Part2 foreach 语句的等价形式(while循环)

1
2
3
4
5
6
7
8
9
10
11
12
foreach(var p in Persons)
{
WriteLine(p);
}
// 等价于一个 while 循环
IEnumerator<Person> enumerator = persons.GetEnumerator();
while(enumerator.MoveNext())
{
Person p = enumerator.Current;
WriteLine(p);
}
  1. 可以看到,这里并没有调用 Reset 方法,此方法通常用于与 COM 的交互操作,许多 .NET 枚举器抛出 NotSupportedException;
  2. 集合可以被 foreach, 不一定需要实现 IEnumerable 接口,有 GetEnumerator 方法即可。
  3. 一个集合类可以提供多个不同的 GetEnumerator 实现,如 GetEnumerator1,GetEnumerator2,返回不同的 IEnumerator,以实现不同的迭代功能。(见下文)

Part3 IEnumerator 与 yield

一个集合类想要支持被迭代,最主要的是构造一个 Enumerator 类,实现 IEnumerator 接口,在 GetEnumerator 方法中返回这个 Enumerator 类。
如此,在 Enumerator 类中,需要维护 Current 属性和 MoveNext 方法,在 MoveNext 方法中,更新 Current 的值,并返回是否还有后续值的 bool 判断。

在实现 IEnumerator 接口时,通常也要实现其泛型版本 IEnumerator

这段文字看起来有点晕,实际上,实现一个 IEnumerator 也是一个苦力活。在实际的编程中,一般直接使用已有的集合元素,不必从头实现一个 IEnumerator 。

yield 是 C# 提供语法糖,可以方便的实现 IEnumerator 接口。如:

1
2
3
4
5
6
7
8
public IEnumerator<string> GetEnumerator()
{
yield return "A";
yield return "B";
yield return "C";
// ...
yield return "Z";
}

这样,实际上就实现了一个集合,这个集合保存了大写的26个字母。

yield return 语句返回集合的一个元素,并移动到下一个元素,相当于同时维护 CurrentMoveNextyield break 可停止迭代。

使用 yield,编译器会创建一个状态机,用于实际维护 CurrentMoveNext

Part4 实现多个不同的 IEnumerator

有一个 MusicCollection 集合类,里面包含了 IList 集合,现在要在其中实现对 Music.Title , Music.Author , Music.Time 进行遍历的支持,可以这么做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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,从而形成多个互相不影响的迭代过程。

在迭代过程中,不能修改迭代集合,否则不安全。


简述c#中可枚举对象和遍历器的工作原理? - 知乎
线程安全的枚举在C#中_C#_编程语言_或代码