C# - 枚举器和迭代器

1,291 阅读4分钟

# 前述

在C#里 , foreach语句可以用来遍历数组中的元素 ,如下所示 :

    int[] arr1 = { 10 , 11 , 12 , 13} ;     //定义数组
    foreach ( int item in arr )             //枚举元素
        Console.WtitLine( $"Item value : {item}" );

这段代码将会产生如下的输出 :

    Item value: 10 
    Item value: 11
    Item value: 12
    Item value: 13

你应该感到好奇 —— 为什么foreach语句可以自动遍历数组中的每一个元素 ? 要回答这个问题 ,我们得先明确 : foreach语句只是一种语法糖 ,或者更通俗的说 —— 它只是一种方便了程序员的简化写法而已 。

所以 ,上述代码的等价非简化写法是 :

    static void Main()
    {
        int[] arr1 = { 10 , 11 , 12 , 13 } ;   //创建数组
        
        //获取数组对象的枚举器 
        IEnumerator ie = arr1.GetEnumerator();
        
        //移到下一项
        while( ie.MoveNext() )  
        {
            int item = (int)ie.Current ;    //ie.current默认是Object类型,转型为int ;
            Console.WriteLine($"Item value: {item}");  //输出
        }
    }

这段代码的输出 ,和上述使用foreach语法糖的代码相一致 :

    Item value: 10 
    Item value: 11
    Item value: 12
    Item value: 13

这段代码才是foreach语法糖的实质 ,也就是说 ,在经过C#编译器的编译之后 ,foreach语句会被编译成这种形式的代码 ,而这也是程序运行期间具体被执行的代码 ;

由上述代码可以看出 ,使用foreach语句遍历数组的本质是 : 通过调用数组对象的GetEnumerator()方法可以获取一个叫做枚举器的对象(实现了IEnumerator接口的类的实例) ,通过操纵这个对象 , 可以依次序的控制枚举器所迭代的数组元素 。而数组类型的getEnumerator()方法是通过继承并且实现IEnumerable接口而来的,我们把实现了IEnumerable接口的类叫做可枚举类型 ,例如数组类就是可枚举类型 ;

我们从上述对foreach的探讨 ,引出了两个全新的概念 —— 枚举器和可枚举类型 ,这也是本文所要探讨的焦点 ,接下来 , 我们将通过一系列的示例 ,来探讨它们的实现原理 和运作机制 ;


#枚举器的本质 - 实现IEnumerator接口的类的实例

在上文中 ,我们提到枚举器是实现了IEnumerator接口的类的实例 ,所以我们来看一下IEnumerator接口长啥样 :

namespace System.Collections
{
    //
    // 摘要:
    //     Supports a simple iteration over a non-generic collection.
    public interface IEnumerator
    {
        //
        // 摘要:
        //     Gets the element in the collection at the current position of the enumerator.
        //
        // 返回结果:
        //     The element in the collection at the current position of the enumerator.
        object? Current { get; }

        //
        // 摘要:
        //     Advances the enumerator to the next element of the collection.
        //
        // 返回结果:
        //     true if the enumerator was successfully advanced to the next element; false if
        //     the enumerator has passed the end of the collection.
        //
        // 异常:
        //   T:System.InvalidOperationException:
        //     The collection was modified after the enumerator was created.
        bool MoveNext();
        //
        // 摘要:
        //     Sets the enumerator to its initial position, which is before the first element
        //     in the collection.
        //
        // 异常:
        //   T:System.InvalidOperationException:
        //     The collection was modified after the enumerator was created.
        void Reset();
    }
}

正如上述代码显示的 ,IEnumerator接口包含了3个需要被继承它的类所实现的函数成员 ,分别是 :

  • Current : 它是一个只读的属性 , 它返回的是object类型的引用 ,所以可以返回任何类型的对象 ,或者通俗点说 —— Current返回序列中的当前位置项
  • MoveNext : 把枚举器位置前进到集合中下一项的方法 。它返回布尔值 ,指示新的位置是有效位置还是已经超过了序列的尾部 ,即 ,如果新的位置是有效的 ,方法返回true ,如果是无效的(比如当前位置到达了尾部),方法返回false 。因为枚举器的原始位置在序列的第一项之前 ,因此MoveNext必须在第一次使用Current之前调用 ;
  • Reset : 把位置重置为原始状态的方法 ;

下面展示一个枚举器类ArrEnumerator ,可以把它看成枚举器类的标准模板, 其继承并且实现了接口IEnumerator :

    using System ;
    using System.Collections ;
    class ArrEnumerator : IEnumerator
    {
        string[] Arrs ;       // 这个Arrs是关键,通过它保存将要被枚举器迭代的序列的副本(其实就是数组) 。 作为演示 ,这里把它置为string[]类型 ,它也可以是int[]、float[]或者其它。
        int position = -1 ;  // 序列(数组元素)位置 ,默认是-1 ;
        
        //构造函数
        public ArrEnumerator( string[] theArrs )  // theArrs是需要被迭代的序列(数组)对象的引用
        {
            // 通过Arrs保存副本 ;注意 : Arrs和theArrs是对不同序列对象的引用 
            Arrs = new string[theArrs.Length] ;
            for( int i = 0 ; i < theArrs.Length ;  i++ )
            {
                Arrs[i] = theArrs[i] ;
            }
        }
        
        //实现Current , 返回枚举器所指向的当前序列元素 ,返回的是一个object类型的引用
        public object Current
        {
            get
            {
                if( position == -1 )
                    throw new InvalidOperationException() ;
                if( position >= Arrs.Length )
                    throw new InvalidOperationException() ;
                    
                return Arrs[position] ;
            }     
        }
        
        //实现MoveNext() ,以便移动到序列的下一个位置
        public bool MoveNext() 
        {
            if( position < Arrs.Length - 1 )
            {
                position++ ;
                return true ;
            }
            else 
            {
                return false ;
            }
        }
        
        // 实现reset ,将枚举器恢复到初始状态
        public void reset()
        {
            position = -1 ;
        }
        
    }

上述代码描述了枚举器类的标准模板,虽然对IEnumerator接口成员的具体实现方式可能不同 。

我们可以从上述代码看出 ,枚举器是有状态的 , 这个状态是用position来描述的 ,position的值不同 , 我们便说它的状态是不同的 。而状态的切换 , 即position值的+1递增 , 是由MoveNext()方法来实现的 。每次MoveNext()之后 , 只要position< arrs.Lenght ,那么新的位置(状态)就是有效的 ,moveNext便会返回true 。否则 , 如果移动到的新的位置(状态)是无效的 ,即position >= arrs.Length , 那么就会返回false ;当状态为 position == arrs.Length时 ,继续调用moveNext()将总会返回false ,此时,如果在调用Current将抛出异常 。


# 可枚举类型的本质 - 实现了IEnumerable接口

可枚举类是指实现了IEnumerable接口的类 。IEnumerable接口只有一个成员 —— GetEnumerator方法 , 它负责返回对象的枚举器 ;

下面的这段代码是IEnumerable接口的具体实现 :

namespace System.Collections
{
    //
    // 摘要:
    //     Exposes an enumerator, which supports a simple iteration over a non-generic collection.
    public interface IEnumerable
    {
        //
        // 摘要:
        //     Returns an enumerator that iterates through a collection.
        //
        // 返回结果:
        //     An System.Collections.IEnumerator object that can be used to iterate through
        //     the collection.
        IEnumerator GetEnumerator();
    }
}

正如前面所说的 , IEnumerable接口只有一个成员 —— 方法 GetEnumerator ;

接下来 , 我们将通过下面的这段代码 ,演示可枚举类型的标准声明形式 ,或者说是标准模板:

    using System.Collections ;
    class MyColors : IEnumerable
    {
        string[] colors = { "Red" , "Yellow" , "Blue" } ;
        
        public IEnumerator GetEnumerator() 
        {
            return new ColorEnumerator(Colors) ;
        }
        
    }