迭代器模式 与 C# IEnumerator/IEnumerable

7/22/2018 csharp

迭代器模式 与 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);
}

  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{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 语句返回集合的一个元素,并移动到下一个元素,相当于同时维护 CurrentMoveNextyield break 可停止迭代。

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

# 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#中可枚举对象和遍历器的工作原理? - 知乎 (opens new window) 线程安全的枚举在C#中_C#_编程语言_或代码 (opens new window)

更新时间: Friday, March 12, 2021 22:54