构建可枚举类型(IEnumerable和IEnumerator)

IEnumerable和IEnumerator

  为了开始对实现既有.NET接口的研究,让我们先看一下IEnumerable和IEnumerator的作用。C#支持关键字foreach,允许我们遍历任何数组类型的内容。虽然看上去只有数组类型才可以使用这个结构,其实任何支持GetEnumerator()方法的类型都可以通过foreach结构进行运算。

  支持这种行为的类或结构实际上是在宣告它们向调用者(如foreach关键字本身)公开了所包含的子项,下面是标准的.NET接口定义:

namespace System.Collections
{
    // 这个接口告知调用方对象的子项可以枚举
    public interface IEnumerable
    {
        IEnumerator GetEnumerator();
    }
}

  可以看到,GetEnumerator()方法返回一个对另一个接口System.Collections.IEnumerator的引用。这个接口提供了基础设施,调用方可以用来移动IEnumerable兼容容器包含的内部对象:

namespace System.Collections
{
    // 这个接口允许调用方获取一个容器的子项
    public interface IEnumerator
    {
        object Current { get; } // 获取当前的项(只读属性)
        bool MoveNext();        // 将光标的内部位置向前移动
        void Reset();           // 将光标重置到第一个成员前
    }
}

   如果想自定义类型支持这些接口,可以手工实现每个方法,这需要花费不少精力。虽然自己开发GetNumerator()、MoveNext()、Current()和Reset()也没问题,但是有一个更简单的方法。因为System.Array类型和其他许多类型已经实现了IEnumerable和IEnumerator接口,你可以简单地将请求委托到System.Array,如下所示:

namespace CustomEnumerator
{
    public class Garage : IEnumerable
    {
        // System.Array已经实现了IEnumerator
        private Car[] carArray = new Car[4];

        public Garage()
        {
            carArray[0] = new Car("Rusty", 30);
            carArray[1] = new Car("Clunker", 55);
            carArray[2] = new Car("Zippy", 30);
            carArray[3] = new Car("Fred", 30);
        }

        public IEnumerator GetEnumerator()
        {
            //  返回数组对象的IEnumerator
            return carArray.GetEnumerator();
        }
    }
}

   修改Garage类型之后,就可以在C# foreach结构中安全使用该类型了。除此之外,GetNumerator()被定义为公开的,对象用户可以与IEnumerator类型交互:

namespace CustomEnumerator
{
    public class Program
    {
        static void Main( string[] args )
        {
            Console.WriteLine("***** Fun with IEnumerable / IEnumerator *****
");
            Garage carLot = new Garage();

            foreach (Car c in carLot)
            {
                Console.WriteLine("{0} is going {1} MPH",
                  c.PetName, c.CurrentSpeed);
            }

            // 手动与IEnumerator协作
            IEnumerator i = carLot.GetEnumerator();
            i.MoveNext();
            Car myCar = (Car)i.Current;
            Console.WriteLine("{0} is going {1} MPH", myCar.PetName, myCar.CurrentSpeed);

            Console.ReadLine();
        }
    }
}

   如果希望在对象级别隐藏IEnumerable的功能,只需要使用显示接口实现就行了:

IEnumerator IEnumerable.GetEnumerator()
{
    return carArray.GetEnumerator();
}

   这样的话,对象用户就不能找到Garage的GetEnumerator()方法,而foreach结构会在必要的时候在背后获得接口。

用yield关键字构建迭代器方法

   在以前,如果我们希望构建支持foreach枚举的自定义集合(如Garage),只能实现IEnumerable接口(可能还有IEnumerator接口)。然后,还可以通过迭代器来构建使用foreach循环的类型。

  简单来说,迭代器就是这样一个成员方法,他指定了容器内部项被foreach处理时该如何返回。虽然迭代器方法还必须命名为GetEnumerator(),返回值还是必须为IEnumerator类型,但自定义类型不需要实现原来那些接口了。

  现在,对当前的Garage类型做如下改进:

    public class Garage
    {
        private Car[] carArray = new Car[4];
        ...// 迭代器方法
        public IEnumerator GetEnumerator()
        {
            foreach (Car c in carArray)
            {
                yield return c;
            }
        }
    }

   注意,这个GetEnumerator()的实现使用内部foreach逻辑迭代每个子项,使用新的yield return语法向调用方返回每个Car对象。yield关键字用来向调用方的foreach结构指定返回值。当到达yield return语句后,当前位置被存储下来,下次调用迭代器时会从这个位置开始执行。

  迭代器方法不一定要通过foreach关键字来返回内容。我们也可以使用如下代码定义迭代器方法:

public IEnumerator GetEnumerator()
{
    yield return carArray[0];
    yield return carArray[1];
    yield return carArray[2];
    yield return carArray[3];
}

  在这个实现中,注意GetEnumerator()方法显示返回新的值给调用者。虽然对于这个示例来说意义不是很大,因为如果我们为carArray成员变量增加更多对象的话,GetEnumerator()方法就不会同步。但是,如果我们希望方法返回能被foreach语法处理的局部数据,这个语法就很有用。

构建命名迭代器

   还有有趣的一点是,yield关键字从技术上说可以结合任何方法一起使用,无论方法名是什么。这些方法(技术上称为命名迭代器)独特之处在于可以接受许多参数。如果构建命名迭代器的话,需要知道这些方法会返回IEnumerable接口,而不是预计的IEnumerator兼容类型。例如,我们可以为Garage类型增加如下方法:

public IEnumerable GetTheCars( bool ReturnRevesed )
{
    // 逆序返回项
    if (ReturnRevesed)
    {
        for (int i = carArray.Length; i != 0; i--)
        {
            yield return carArray[i - 1];
        }
    }
    else
    {
        // 按顺序返回数组中的项
        foreach (Car c in carArray)
        {
            yield return c;
        }
    }
}

   注意,我们的新方法允许调用者以正序和逆序(如果传入的参数值为true)来获取子项。我们可以按如下所示的代码和新方法进行交互:

class Program
{
    static void Main( string[] args )
    {
        Console.WriteLine("***** Fun with the Yield Keyword *****
");
        Garage carLot = new Garage();

        // 使用GetEnumerator()来获取项
        foreach (Car c in carLot)
        {
            Console.WriteLine("{0} is going {1} MPH",
                c.PetName, c.CurrentSpeed);
        }

        Console.WriteLine();

        // 使用命名迭代器来获取项(逆序)
        foreach (Car c in carLot.GetTheCars(true))
        {
            Console.WriteLine("{0} is going {1} MPH",
                c.PetName, c.CurrentSpeed);
        }
        Console.ReadLine();
    }
}

  命名迭代器是很有用的结构,因为一个自定义容器可以定义多重方式来请求返回的集。

  那么,总结一下可枚举对象的构建吧。记住,如果自定义类型要和C#的foreach关键字一起使用的话,容器就需要定义一个名为GetEnumerator()的方法,它由IEnumerator接口类型来定制。通常,这个方法的实现只是交给保存子对象的内部成员,然而,我们也可以使用yield return语法来提供多个"命名迭代器"方法。

原文地址:https://www.cnblogs.com/gyt-xtt/p/6270160.html