迭代器 (C#)

迭代器可用于单步执行列表和数组等集合。

迭代器方法或 get 访问器对集合执行自定义迭代。 迭代器方法使用 yield return 语句一次返回每个元素。 到达 yield return 语句时,会记住当前在代码中的位置。 下次调用迭代器函数时,将从该位置重启执行。

通过在客户端代码中使用 foreach 语句或 LINQ 查询来消耗迭代器。

在以下示例中,foreach 循环的第一次迭代会使执行从SomeNumbers迭代器方法继续,直到遇到第一个yield return语句。 此迭代返回值 3,并保留迭代器方法中的当前位置。 在循环的下一次迭代中,迭代器方法中的执行从其离开的位置继续执行,在到达 yield return 语句时再次停止。 此迭代返回值 5,迭代器方法中的当前位置再次保留。 当到达迭代器方法的末尾时,循环将完成。

static void Main()
{
    foreach (int number in SomeNumbers())
    {
        Console.Write(number.ToString() + " ");
    }
    // Output: 3 5 8
    Console.ReadKey();
}

public static System.Collections.IEnumerable SomeNumbers()
{
    yield return 3;
    yield return 5;
    yield return 8;
}

迭代器方法或get访问器的返回类型可以是IEnumerableIEnumerable<T>IEnumeratorIEnumerator<T>

可以使用 yield break 语句结束迭代。

注释

对于本主题中除简单迭代器示例以外的所有示例,请为 System.Collections 命名空间加入 System.Collections.Generic 指令。

简单迭代器

以下示例包含一个位于yield return循环内的语句。 在 Main 中,foreach 语句体的每次迭代都会创建一个对迭代器函数的调用,并将继续到下一个 yield return 语句。

static void Main()
{
    foreach (int number in EvenSequence(5, 18))
    {
        Console.Write(number.ToString() + " ");
    }
    // Output: 6 8 10 12 14 16 18
    Console.ReadKey();
}

public static System.Collections.Generic.IEnumerable<int>
    EvenSequence(int firstNumber, int lastNumber)
{
    // Yield even numbers in the range.
    for (int number = firstNumber; number <= lastNumber; number++)
    {
        if (number % 2 == 0)
        {
            yield return number;
        }
    }
}

创建集合类

在下面的示例中,DaysOfTheWeek类实现了IEnumerable接口,该接口要求有GetEnumerator方法。 编译器隐式调用 GetEnumerator 方法,该方法返回一个 IEnumerator

GetEnumerator 方法通过使用 yield return 语句每次返回 1 个字符串。

static void Main()
{
    DaysOfTheWeek days = new DaysOfTheWeek();

    foreach (string day in days)
    {
        Console.Write(day + " ");
    }
    // Output: Sun Mon Tue Wed Thu Fri Sat
    Console.ReadKey();
}

public class DaysOfTheWeek : IEnumerable
{
    private string[] days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];

    public IEnumerator GetEnumerator()
    {
        for (int index = 0; index < days.Length; index++)
        {
            // Yield each day of the week.
            yield return days[index];
        }
    }
}

以下示例创建一个 Zoo 包含动物集合的类。

foreach引用类实例 (theZoo) 的语句隐式调用GetEnumerator该方法。 引用 foreachBirds 属性的 Mammals 语句使用 AnimalsForType 命名迭代器方法。

static void Main()
{
    Zoo theZoo = new Zoo();

    theZoo.AddMammal("Whale");
    theZoo.AddMammal("Rhinoceros");
    theZoo.AddBird("Penguin");
    theZoo.AddBird("Warbler");

    foreach (string name in theZoo)
    {
        Console.Write(name + " ");
    }
    Console.WriteLine();
    // Output: Whale Rhinoceros Penguin Warbler

    foreach (string name in theZoo.Birds)
    {
        Console.Write(name + " ");
    }
    Console.WriteLine();
    // Output: Penguin Warbler

    foreach (string name in theZoo.Mammals)
    {
        Console.Write(name + " ");
    }
    Console.WriteLine();
    // Output: Whale Rhinoceros

    Console.ReadKey();
}

public class Zoo : IEnumerable
{
    // Private members.
    private List<Animal> animals = new List<Animal>();

    // Public methods.
    public void AddMammal(string name)
    {
        animals.Add(new Animal { Name = name, Type = Animal.TypeEnum.Mammal });
    }

    public void AddBird(string name)
    {
        animals.Add(new Animal { Name = name, Type = Animal.TypeEnum.Bird });
    }

    public IEnumerator GetEnumerator()
    {
        foreach (Animal theAnimal in animals)
        {
            yield return theAnimal.Name;
        }
    }

    // Public members.
    public IEnumerable Mammals
    {
        get { return AnimalsForType(Animal.TypeEnum.Mammal); }
    }

    public IEnumerable Birds
    {
        get { return AnimalsForType(Animal.TypeEnum.Bird); }
    }

    // Private methods.
    private IEnumerable AnimalsForType(Animal.TypeEnum type)
    {
        foreach (Animal theAnimal in animals)
        {
            if (theAnimal.Type == type)
            {
                yield return theAnimal.Name;
            }
        }
    }

    // Private class.
    private class Animal
    {
        public enum TypeEnum { Bird, Mammal }

        public string Name { get; set; }
        public TypeEnum Type { get; set; }
    }
}

将迭代器与泛型列表配合使用

在以下示例中, Stack<T> 泛型类实现 IEnumerable<T> 泛型接口。 该方法 Push 将值分配给类型的 T数组。 该方法 GetEnumerator 使用 yield return 语句返回数组值。

除了泛型 GetEnumerator 方法,还必须实现非泛型 GetEnumerator 方法。 这是因为 IEnumerable<T> 继承自 IEnumerable. 非泛型实现遵从泛型实现的规则。

该示例使用命名迭代器来支持遍历同一数据收集的各种方式。 这些命名迭代器包括 TopToBottomBottomToTop 属性,以及 TopN 方法。

BottomToTop 属性在一个 get 访问器中使用迭代器。

static void Main()
{
    Stack<int> theStack = new Stack<int>();

    //  Add items to the stack.
    for (int number = 0; number <= 9; number++)
    {
        theStack.Push(number);
    }

    // Retrieve items from the stack.
    // foreach is allowed because theStack implements IEnumerable<int>.
    foreach (int number in theStack)
    {
        Console.Write("{0} ", number);
    }
    Console.WriteLine();
    // Output: 9 8 7 6 5 4 3 2 1 0

    // foreach is allowed, because theStack.TopToBottom returns IEnumerable(Of Integer).
    foreach (int number in theStack.TopToBottom)
    {
        Console.Write("{0} ", number);
    }
    Console.WriteLine();
    // Output: 9 8 7 6 5 4 3 2 1 0

    foreach (int number in theStack.BottomToTop)
    {
        Console.Write("{0} ", number);
    }
    Console.WriteLine();
    // Output: 0 1 2 3 4 5 6 7 8 9

    foreach (int number in theStack.TopN(7))
    {
        Console.Write("{0} ", number);
    }
    Console.WriteLine();
    // Output: 9 8 7 6 5 4 3

    Console.ReadKey();
}

public class Stack<T> : IEnumerable<T>
{
    private T[] values = new T[100];
    private int top = 0;

    public void Push(T t)
    {
        values[top] = t;
        top++;
    }
    public T Pop()
    {
        top--;
        return values[top];
    }

    // This method implements the GetEnumerator method. It allows
    // an instance of the class to be used in a foreach statement.
    public IEnumerator<T> GetEnumerator()
    {
        for (int index = top - 1; index >= 0; index--)
        {
            yield return values[index];
        }
    }

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

    public IEnumerable<T> TopToBottom
    {
        get { return this; }
    }

    public IEnumerable<T> BottomToTop
    {
        get
        {
            for (int index = 0; index <= top - 1; index++)
            {
                yield return values[index];
            }
        }
    }

    public IEnumerable<T> TopN(int itemsFromTop)
    {
        // Return less than itemsFromTop if necessary.
        int startIndex = itemsFromTop >= top ? 0 : top - itemsFromTop;

        for (int index = top - 1; index >= startIndex; index--)
        {
            yield return values[index];
        }
    }

}

语法信息

迭代器可用作一种方法,或一个 get 访问器。 迭代器不能在事件、实例构造函数、静态构造函数或静态终结器中发生。

从语句中的 yield return 表达式类型到迭代器返回的类型参数 IEnumerable<T> ,必须存在隐式转换。

在 C# 中,迭代器方法不能有任何inrefout参数。

在 C# 中,yield 不是保留字,仅在紧接在 returnbreak 关键字之前使用时才具有特殊含义。

技术实现

尽管将迭代器编写为方法,但编译器会将它转换为实际上为状态机的嵌套类。 只要客户端代码中的 foreach 循环继续,此类就会跟踪迭代器的位置。

若要查看编译器的作用,可以使用 Ildasm.exe 工具查看为迭代器方法生成的公共中间语言代码。

结构创建迭代器时,无需实现整个 IEnumerator 接口。 当编译器检测到迭代器时,它会自动生成CurrentMoveNext接口中的DisposeIEnumeratorIEnumerator<T>方法。

foreach 循环(或对 IEnumerator.MoveNext 的直接调用)的每次后续迭代中,下一个迭代器代码体都会在上一个 yield return 语句之后恢复。 然后,它会继续执行下一个 yield return 语句,直到迭代器正文结束,或者遇到 yield break 语句为止。

迭代器不支持该方法 IEnumerator.Reset 。 若要从一开始就重申,必须获取新的迭代器。 在迭代器方法返回的迭代器上调用 Reset 会引发 NotSupportedException

有关详细信息,请参阅 C# 语言规范

使用迭代器

当需要使用复杂代码填充列表序列时,迭代器使你能够保持循环的 foreach 简单性。 若要执行以下操作,这会很有帮助:

  • 在第一次 foreach 循环迭代后修改列表序列。

  • 避免在foreach循环的第一次迭代之前完全加载大型列表。 一个示例是用于加载一批表格行的分页提取。 另一个示例是在 EnumerateFiles .NET 中实现迭代器的方法。

  • 在迭代器中封装列表的构建过程。 在迭代器方法中,可以生成列表,然后在循环中生成每个结果。

另请参阅