c# 高级编程 7章160页 【foreach的原理】【IEnumerator接口】【yield语句】

223 阅读2分钟

前言:

  • c# 1.0以来,foreach就可以直接用来迭代数组和集合
  • 但对于一个自定义类,要用foreach来迭代的话,需要创建枚举器Enumerator, yield语句就是用来方便创建枚举器的。
  • c# 2.0添加了yield语句。

foreach遍历X类型的原理:

  • X类型,实现了IEnumerable接口
  • IEnumerable接口定义了一个方法GetEnumerator()
  • GetEnumerator()方法返回一个实现了IEnumerator接口的类型实例
  • foreach就可以使用这个实现了IEnumerator接口的类型实例来遍历X类型

虽然有上述内容,但是要用foreach来遍历X类型,实际上并不强制要求在X类中必须实现IEnumerable接口。而是,只要在X类中有GetEnumerator()方法,就足矣

这个GetEnumerator()方法,照例,还是返回一个实现了IEnumerator接口的类型实例。重要的是,X类得有一个GetEnumerator()方法


IEnumerator接口

  • 定义了Current属性, 是当前元素
  • 定义了MoveNext(),移动到下一个元素
    • 如果有这个元素,则返回true
    • 如果不再有更多元素,则返回false
  • 其泛型类,定义了Dispose()

foreach,会被编译器进一步转换成IEnumerator接口的方法和属性:

如下例:遍历一个自定义类Person为元素的数组:

foreach(var person in persons)
{
    Console.WriteLine(person);
}

会被c#编译器解析为:

IEnumerator<Person> enumerator = persons.GetEnumerator();

while(enumerator.MoveNext())
{
    Person person = enumerator.CurrentConsole.WriteLine(person);
}

yeild 语句

  • yield return语句,返回集合的一个元素,并移动到下一个元素上。
  • yield break语句,停止迭代。
public class HelloCollection
{
    public IEnumerator<string> GetEnumerator()
    {
        yield return "Hello";
        yield return "World";
    }
}
public void HelloWorld()
{
    var helloCollection = new HelloCollection();
    foreach(var s in helloCollection)
    {
        Console.WriteLine(s);
    }
}

以下代码被称作,迭代块

    public IEnumerator<string> GetEnumerator()
    {
        yeild return "Hello";
        yeild return "World";
    }

迭代块

包含yield 语句的方法或属性,被称为迭代块

迭代块必须声明为返回IEnumerator或IEnumerable接口(或它们的泛型版本)

迭代块可包含多条yield return和yield break语句,但不能包含return语句


迭代块,会被编译器进一步转换成,内部Yield类型

public class HelloCollection
{
    public IEnumerator<string> GetEnumerator()
    {
        yield return "Hello";
        yield return "World";
    }
}

转换:

  • 编译器会生成一个yield类型,如下是Enumerator类
  • HelloCollection的GetEnumerator()方法会返回一个Enumerator类的实例

Enumerator类

  • 是HelloCollection类的内部类
  • 实现了IEnumeratorIDisposable接口
  • 包含一个状态机, _state
  • MoveNext()方法 封装了迭代块的内容,并设置_current
public class HelloCollection
{
    public IEnumerator GetEnumerator() => new Enumerator(0);
    
    public class Enumerator: IEnumerator<string>, IEnumerator, IDisposable
    {
        private int _state;
        private string _current;
        
        public Enumerator(int state) => _state = state;
        bool System.Collections.IEnumerator.MoveNext()
        {
            switch(_state)
            {
                case 0:
                    _current = "Hello";
                    _state = 1;
                    return true;
                case 1:
                    _current = "World";
                    _state = 2;
                    return true;
                case 2:
                    break;
            }
            return false;
        }
        
        void System.Collections.IEnumerator.Reset => throw new NotSupportedException();
        
        string System.Collections.IEnumerator<string>.Current => _current;
        
        object System.Collections.IEnumerator.Current => _current;
        
        void IDisposable.Dispose() { }
    }
}

yield使用案例 - 遍历方式的优雅封装

public class MusicTitles
{
    string[] names = {"Tubular Bells", "Hergest Ridge", "Ommadawn", "Platinum"};
    
    public IEnumerator<string> GetEnumerator()
    {
        for (int i = 0; i < 4; i++)
        {
            yield return names[i];
        }
    }
    
    public IEnumerable<string> Reverse()
    {
        for (int i = 3; i >= 0; i--)
        {
            yield return names[i];
        }
    }
    
    public IEnumerable<string> Subset(int index, int length)
    {
        for (int i = index; i < index + length; i++)
        {
            yield return names[i];
        }
    }
}
var titles = new MusicTitles();
foreach(var title in titles)
{
    Console.WriteLine(title);
}
Console.WriteLine();

Console.WriteLine("reverse");
foreach(var title in titles.Reverse())
{
    Console.WriteLine(title);
}

Console.WriteLine("subset");
foreach(var title in titles.Subset(2,2))
{
    Console.WriteLine(title);
}

yield 使用案例 - 轮流下棋

public class GameMoves
{
    private IEnumerator _cross;
    private IEnumerator _circle;
    public GameMoves()
    {
        _cross = Cross();
        _circle = Circle();
    }
    private int _move = 0;
    const int MaxMoves = 9;
    
    public IEnumerator Cross()
    {
        while(true)
        {
            Console.WriteLine($"Cross, move {_move}");
            if (++_move >= MaxMoves)
            {
                yield break;
            }
            yield return _circle;
        }
    }
    
    public IEnumerator Circles()
    {
        while(true)
        {
            Console.WriteLine($"Circle, move {_move}");
            if (++_move >= MaxMoves)
            {
                yield break;
            }
            yield return _cross;
        }
    }
}
var game  = new GameMoves();
IEnumerator enumerator = game.Cross();
while(enumerator.MoveNext())
{
    enumerator = enumerator.Current as IEnumerator;
}