迭代器模式 与 C# IEnumerator/IEnumerable
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{T}。
这段文字看起来有点晕,实际上,实现一个 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{Music} 集合,现在要在其中实现对 Music.Title , Music.Author , Music.Time 进行遍历的支持,可以这么做:
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#_编程语言_或代码
原文链接: https://blog.jgrass.cc/posts/csharp-enumerator/
本作品采用 「署名 4.0 国际」 许可协议进行许可,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。