C# 中yield 的使用详解

总目录


前言

当我们编写 C# 代码时,经常需要处理大量的数据集合。在传统的方式中,我们往往需要先将整个数据集合加载到内存中,然后再进行操作。但是如果数据集合非常大,这种方式就会导致内存占用过高,甚至可能导致程序崩溃。

C# 中的yield return机制可以帮助我们解决这个问题。通过使用yield return,我们可以将数据集合按需生成,而不是一次性生成整个数据集合。这样可以大大减少内存占用,并且提高程序的性能。


一、IEnumerable 和 IEnumerator

1. IEnumerable

IEnumerable 接口,是可枚举的所有非泛型集合的基接口

公开枚举数,该枚举数支持在非泛型集合上进行简单迭代。其泛型等效项是 System.Collections.Generic.IEnumerable<T>接口

csharp 复制代码
	public interface IEnumerable
    {
        //返回可循环遍历的集合。
        IEnumerator GetEnumerator();
    }

2. IEnumerator

IEnumerator 接口,是所有非泛型枚举器的基接口

支持对非泛型集合的简单迭代,其泛型等效项是 System.Collections.Generic.IEnumerator<T> 接口。

csharp 复制代码
	//支持对非泛型集合进行简单迭代。
    public interface IEnumerator
    {
        // 获取集合中枚举数当前位置的元素。
        object? Current { get; }

        // 将枚举数前进到集合的下一个元素。
        // 返回结果: 如果枚举数成功推进到下一个元素,则为True;如果枚举数已经过集合的末尾,则为False。
        bool MoveNext();

        // 将枚举数设置为其初始位置,即在集合中的第一个元素之前。
        void Reset();
    }

3. 作用

IEnumerable 和 IEnumerator 一般用于实现自定义集合。一个容器Collection要支持foreach方式的遍历,必须实现IEnumerable接口或者必须以某种方式返回IEnumerator object来实现。

通俗讲:C#代码中可以使用Foreach 循环遍历一个List<T> 或者是数组,是因为这些对象实现了IEnumerable 接口,而我们一般自定义的class 如果不实现IEnumerable 接口,是不支持通过foreach循环遍历的,即是说如果我们需要自定义的集合对象能支持循环遍历,就需要实现IEnumerable 接口。

4. 示例

下面的代码示例演示了通过实现 IEnumerable 和 IEnumerator 接口来循环访问自定义集合的最佳做法。

csharp 复制代码
// Simple business object.
public class Person
{
    public Person(string fName, string lName)
    {
        this.firstName = fName;
        this.lastName = lName;
    }

    public string firstName;
    public string lastName;
}

// Person对象的集合。此类实现了IEnumerable,因此可以与ForEach语法一起使用。
public class People : IEnumerable
{
    private Person[] _people;
    public People(Person[] pArray)
    {
        _people = new Person[pArray.Length];

        for (int i = 0; i < pArray.Length; i++)
        {
            _people[i] = pArray[i];
        }
    }

    // GetEnumerator方法的实现
    IEnumerator IEnumerable.GetEnumerator()
    {
        return (IEnumerator)GetEnumerator();
    }

    public PeopleEnum GetEnumerator()
    {
        return new PeopleEnum(_people);
    }
}

// 实现IEnumerable时,还必须实现IEnumerator。
public class PeopleEnum : IEnumerator
{
    public Person[] _people;

    // Enumerators are positioned before the first element
    // until the first MoveNext() call.
    int position = -1;

    public PeopleEnum(Person[] list)
    {
        _people = list;
    }

    public bool MoveNext()
    {
        position++;
        return (position < _people.Length);
    }

    public void Reset()
    {
        position = -1;
    }

    object IEnumerator.Current
    {
        get
        {
            return Current;
        }
    }

    public Person Current
    {
        get
        {
            try
            {
                return _people[position];
            }
            catch (IndexOutOfRangeException)
            {
                throw new InvalidOperationException();
            }
        }
    }
}

调用:

csharp 复制代码
	static void Main()
    {
        Person[] peopleArray = new Person[3]
        {
            new Person("John", "Smith"),
            new Person("Jim", "Johnson"),
            new Person("Sue", "Rabon"),
        };

        People peopleList = new People(peopleArray);
        foreach (Person p in peopleList)
            Console.WriteLine(p.firstName + " " + p.lastName);
    }

二、迭代器

1. 基本介绍

  • 迭代器可用于逐步迭代集合,例如列表和数组。

  • 通过 yield 返回的 IEnumerable<T> 类型,表示这是一个可以被遍历的数据集合。它之所以可以被遍历,是因为它实现了一个标准的 IEnumerable 接口。一般,我们把像上面这种包含 yield 语句并返回 IEnumerable<T> 类型的方法称为迭代器(Iterator)。【注意:包含 yield 语句的方法的返回类型也可以是 IEnumerator<T>,它比迭代器更低一个层级,迭代器是列举器的一种实现。】

  • 迭代器方法或 get 访问器可对集合执行自定义迭代。 迭代器方法使用 yield return 语句返回元素,每次返回一个。 到达 yield return 语句时,会记住当前在代码中的位置。 下次调用迭代器函数时,将从该位置重新开始执行。(通俗讲就是使用 yield return 上下文关键字的方法就是迭代器方法)

  • 通过 foreach 语句或 LINQ 查询从客户端代码中使用迭代器。

在以下示例中,foreach 循环的首次迭代导致 SomeNumbers 迭代器方法继续执行,直至到达第一个 yield return 语句。 此迭代返回的值为 3,并保留当前在迭代器方法中的位置。 在循环的下次迭代中,迭代器方法的执行将从其暂停的位置继续,直至到达 yield return 语句后才会停止。 此迭代返回的值为 5,并再次保留当前在迭代器方法中的位置。 到达迭代器方法的结尾时,循环便已完成。

csharp 复制代码
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 访问器的返回类型可以是 IEnumerable、IEnumerable<T>、IEnumerator 或 IEnumerator<T>。

可以使用 yield break 语句来终止迭代。

2. 简单的迭代器

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

csharp 复制代码
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;
        }
    }
}

3. 创建集合类

在以下示例中,DaysOfTheWeek 类实现 IEnumerable 接口,此操作需要 GetEnumerator 方法。 编译器隐式调用 GetEnumerator 方法,此方法返回 IEnumerator。

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

csharp 复制代码
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 类。

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

csharp 复制代码
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; }
    }
}

4. 对泛型列表使用迭代器

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

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

本示例使用命名迭代器来支持通过各种方法循环访问同一数据集合。 这些命名迭代器为 TopToBottom 和 BottomToTop 属性,以及 TopN 方法。

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

csharp 复制代码
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];
        }
    }

}

5. 使用注意事项

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

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

在 C# 中,迭代器方法不能有任何 in、ref 或 out 参数。

在 C# 中,yield 不是保留字,只有在 return 或 break 关键字之前使用时才有特殊含义。

三、迭代器二

1. 基本介绍

迭代器是遍历容器的对象,尤其是列表。 迭代器可用于:

  • 对集合中的每个项执行操作。
  • 枚举自定义集合。
  • 扩展 LINQ 或其他库。
  • 创建数据管道,以便数据通过迭代器方法在管道中有效流动。
  • C# 语言提供用于生成和使用序列的功能。 可以同步或异步生成和使用这些序列。

2. 使用 foreach 执行循环访问

枚举集合非常简单:使用 foreach 关键字枚举集合,从而为集合中的每个元素执行一次嵌入语句:

csharp 复制代码
foreach (var item in collection)
{
    Console.WriteLine(item?.ToString());
}

就这样。 若要循环访问集合中的所有内容,只需使用 foreach 语句。 但 foreach 语句并非完美无缺。 它依赖于 .NET Core 库中定义的 2 个泛型接口,才能生成循环访问集合所需的代码:IEnumerable<T> 和 IEnumerator<T>。 下文对此机制进行了更详细说明。

这 2 种接口还具备相应的非泛型接口:IEnumerable 和 IEnumerator。 泛型版本是新式代码的首要选项。

异步生成序列时,可以使用 await foreach 语句异步使用此序列:

csharp 复制代码
await foreach (var item in asyncSequence)
{
Console.WriteLine(item?.ToString());
}

如果序列是 System.Collections.Generic.IEnumerable<T>,则使用 foreach。 如果序列是 System.Collections.Generic.IAsyncEnumerable<T>,则使用 await foreach。 在后一种情况下,序列是异步生成的。

3. 使用迭代器方法的枚举源

借助 C# 语言的另一个强大功能,能够生成创建枚举源的方法。 这些方法称为"迭代器方法"。 迭代器方法用于定义请求时如何在序列中生成对象。 使用 yield return 上下文关键字定义迭代器方法。

可编写此方法以生成从 0 到 9 的整数序列:

csharp 复制代码
public IEnumerable<int> GetSingleDigitNumbers()
{
    yield return 0;
    yield return 1;
    yield return 2;
    yield return 3;
    yield return 4;
    yield return 5;
    yield return 6;
    yield return 7;
    yield return 8;
    yield return 9;
}

上方的代码显示了不同的 yield return 语句,以强调可在迭代器方法中使用多个离散 yield return 语句这一事实。 可以使用其他语言构造来简化迭代器方法的代码,这也是一贯的做法。 以下方法定义可生成完全相同的数字序列:

csharp 复制代码
public IEnumerable<int> GetSingleDigitNumbersLoop()
{
    int index = 0;
    while (index < 10)
        yield return index++;
}

不必从中选择一个。 可根据需要提供尽可能多的 yield return 语句来满足方法需求:

csharp 复制代码
public IEnumerable<int> GetSetsOfNumbers()
{
    int index = 0;
    while (index < 10)
        yield return index++;

    yield return 50;

    index = 100;
    while (index < 110)
        yield return index++;
}

上述所有示例都有一个异步对应项。 在每种情况下,将 IEnumerable 的返回类型替换为 IAsyncEnumerable。 例如,前面的示例将具有以下异步版本:

csharp 复制代码
public async IAsyncEnumerable<int> GetSetsOfNumbersAsync()
{
    int index = 0;
    while (index < 10)
        yield return index++;

    await Task.Delay(500);

    yield return 50;

    await Task.Delay(500);

    index = 100;
    while (index < 110)
        yield return index++;
}

这是同步和异步迭代器的语法。 我们来看一个真实示例。 假设你正在处理一个 IoT 项目,设备传感器生成了大量数据流。 为了获知数据,需要编写一个对每第 N 个数据元素进行采样的方法。 通过以下小迭代器方法可实现此目的:

csharp 复制代码
public static IEnumerable<T> Sample<T>(this IEnumerable<T> sourceSequence, int interval)
{
    int index = 0;
    foreach (T item in sourceSequence)
    {
        if (index++ % interval == 0)
            yield return item;
    }
}

如果从 IoT 设备读取生成异步序列,则修改方法,如以下方法所示:

csharp 复制代码
public static async IAsyncEnumerable<T> Sample<T>(this IAsyncEnumerable<T> sourceSequence, int interval)
{
    int index = 0;
    await foreach (T item in sourceSequence)
    {
        if (index++ % interval == 0)
            yield return item;
    }
}

迭代器方法有一个重要限制:在同一方法中不能同时使用 return 语句和 yield return 语句。 以下代码无法编译:

csharp 复制代码
public IEnumerable<int> GetSingleDigitNumbers()
{
    int index = 0;
    while (index < 10)
        yield return index++;

    yield return 50;

    // generates a compile time error:
    var items = new int[] {100, 101, 102, 103, 104, 105, 106, 107, 108, 109 };
    return items;
}

此限制通常不是问题。 可以选择在整个方法中使用 yield return,或选择将原始方法分成多个方法,一些使用 return,另一些使用 yield return。

可略微修改一下最后一个方法,使其可在任何位置使用 yield return:

csharp 复制代码
public IEnumerable<int> GetFirstDecile()
{
    int index = 0;
    while (index < 10)
        yield return index++;

    yield return 50;

    var items = new int[] {100, 101, 102, 103, 104, 105, 106, 107, 108, 109 };
    foreach (var item in items)
        yield return item;
}

有时,正确的做法是将迭代器方法拆分成 2 个不同的方法。 一个使用 return,另一个使用 yield return。 考虑这样一种情况:需要基于布尔参数返回一个空集合,或者返回前 5 个奇数。 可编写类似以下 2 种方法的方法:

csharp 复制代码
public IEnumerable<int> GetSingleDigitOddNumbers(bool getCollection)
{
    if (getCollection == false)
        return new int[0];
    else
        return IteratorMethod();
}

private IEnumerable<int> IteratorMethod()
{
    int index = 0;
    while (index < 10)
    {
        if (index % 2 == 1)
            yield return index;
        index++;
    }
}

看看上面的方法。 第 1 个方法使用标准 return 语句返回空集合,或返回第 2 个方法创建的迭代器。 第 2 个方法使用 yield return 语句创建请求的序列

四、yield

1. 基本介绍

yield 语句 - 提供下一个元素

在迭代器中使用 yield 语句提供下一个值或表示迭代结束。

2. yield return 和 yield break

yield 语句有以下两种形式:

  • yield return:在迭代中提供下一个值,如以下示例所示:
csharp 复制代码
foreach (int i in ProduceEvenNumbers(9))
{
    Console.Write(i);
    Console.Write(" ");
}
// Output: 0 2 4 6 8

IEnumerable<int> ProduceEvenNumbers(int upto)
{
    for (int i = 0; i <= upto; i += 2)
    {
        yield return i;
    }
}
  • yield break:显式示迭代结束,如以下示例所示:
csharp 复制代码
Console.WriteLine(string.Join(" ", TakeWhilePositive(new int[] {2, 3, 4, 5, -1, 3, 4})));
// Output: 2 3 4 5

Console.WriteLine(string.Join(" ", TakeWhilePositive(new int[] {9, 8, 7})));
// Output: 9 8 7

IEnumerable<int> TakeWhilePositive(IEnumerable<int> numbers)
{
    foreach (int n in numbers)
    {
        if (n > 0)
        {
            yield return n;
        }
        else
        {
            yield break;
        }
    }
}

当控件到达迭代器的末尾时,迭代也结束。

3. yield 的使用

在前面的示例中,迭代器的返回类型为 IEnumerable<T>(在非泛型情况下,使用 IEnumerable 作为迭代器的返回类型)。 还可以使用 IAsyncEnumerable<T> 作为迭代器的返回类型。 这使得迭代器异步。 使用 await foreach 语句对迭代器的结果进行迭代,如以下示例所示:

csharp 复制代码
await foreach (int n in GenerateNumbersAsync(5))
{
    Console.Write(n);
    Console.Write(" ");
}
// Output: 0 2 4 6 8

async IAsyncEnumerable<int> GenerateNumbersAsync(int count)
{
    for (int i = 0; i < count; i++)
    {
        yield return await ProduceNumberAsync(i);
    }
}

async Task<int> ProduceNumberAsync(int seed)
{
    await Task.Delay(1000);
    return 2 * seed;
}

迭代器的返回类型可以是 IEnumerator<T> 或 IEnumerator。 在以下方案中实现 GetEnumerator 方法时,请使用这些返回类型:

  • 设计实现 IEnumerable<T> 或 IEnumerable 接口的类型。

  • 添加实例或扩展 GetEnumerator 方法来使用 foreach 语句对类型的实例启用迭代,如以下示例所示:

csharp 复制代码
public static void Example()
{
    var point = new Point(1, 2, 3);
    foreach (int coordinate in point)
    {
        Console.Write(coordinate);
        Console.Write(" ");
    }
    // Output: 1 2 3
}

public readonly record struct Point(int X, int Y, int Z)
{
    public IEnumerator<int> GetEnumerator()
    {
        yield return X;
        yield return Y;
        yield return Z;
    }
}

不能在下列情况中使用 yield 语句:

  • 带有 in、ref 或 out 参数的方法
  • Lambda 表达式和匿名方法
  • 不安全块。 在 C# 13 之前,yield 在具有 unsafe 块的任何方法中都无效。 从 C# 13 开始,可以在
  • 包含 unsafe 块的方法中使用 yield,但不能在 unsafe 块中使用。

4. 迭代器的执行

迭代器的调用不会立即执行,如以下示例所示:

csharp 复制代码
var numbers = ProduceEvenNumbers(5);
Console.WriteLine("Caller: about to iterate.");
foreach (int i in numbers)
{
    Console.WriteLine($"Caller: {i}");
}

IEnumerable<int> ProduceEvenNumbers(int upto)
{
    Console.WriteLine("Iterator: start.");
    for (int i = 0; i <= upto; i += 2)
    {
        Console.WriteLine($"Iterator: about to yield {i}");
        yield return i;
        Console.WriteLine($"Iterator: yielded {i}");
    }
    Console.WriteLine("Iterator: end.");
}
// Output:
// Caller: about to iterate.
// Iterator: start.
// Iterator: about to yield 0
// Caller: 0
// Iterator: yielded 0
// Iterator: about to yield 2
// Caller: 2
// Iterator: yielded 2
// Iterator: about to yield 4
// Caller: 4
// Iterator: yielded 4
// Iterator: end.

如前面的示例所示,当开始对迭代器的结果进行迭代时,迭代器会一直执行,直到到达第一个 yield return 语句为止。 然后,迭代器的执行会暂停,调用方会获得第一个迭代值并处理该值。 在后续的每次迭代中,迭代器的执行都会在导致上一次挂起的 yield return 语句之后恢复,并继续执行,直到到达下一个 yield return 语句为止。 当控件到达迭代器或 yield break 语句的末尾时,迭代完成。

五、其他

yield return将数据集合按需生成,而不是一次性生成整个数据集合。接下来通过一个简单的示例,我们看一下它的工作方式是什么样的,以便加深对它的理解

csharp 复制代码
foreach (var num in GetInts())
{
    Console.WriteLine("外部遍历了:{0}", num);
}

IEnumerable<int> GetInts()
{
    for (int i = 0; i < 5; i++)
    {
        Console.WriteLine("内部遍历了:{0}", i);
        yield return i;
    }
}

首先,在GetInts方法中,我们使用yield return关键字来定义一个迭代器。这个迭代器可以按需生成整数序列。在每次循环时,使用yield return返回当前的整数。通过1foreach循环来遍历 GetInts方法返回的整数序列。在迭代时GetInts方法会被执行,但是不会将整个序列加载到内存中。而是在需要时,按需生成序列中的每个元素。在每次迭代时,会输出当前迭代的整数对应的信息。所以输出的结果为

csharp 复制代码
内部遍历了:0
外部遍历了:0
内部遍历了:1
外部遍历了:1
内部遍历了:2
外部遍历了:2
内部遍历了:3
外部遍历了:3
内部遍历了:4
外部遍历了:4

可以看到,整数序列是按需生成的,并且在每次生成时都会输出相应的信息。这种方式可以大大减少内存占用,并且提高程序的性能。当然从c# 8开始异步迭代的方式同样支持

csharp 复制代码
await foreach (var num in GetIntsAsync())
{
    Console.WriteLine("外部遍历了:{0}", num);
}

async IAsyncEnumerable<int> GetIntsAsync()
{
    for (int i = 0; i < 5; i++)
    {
        await Task.Yield();
        Console.WriteLine("内部遍历了:{0}", i);
        yield return i;
    }
}

和上面不同的是,如果需要用异步的方式,我们需要返回IAsyncEnumerable类型,这种方式的执行结果和上面同步的方式执行的结果是一致的,我们就不做展示了。


结语

以上就是本文的内容,希望以上内容可以帮助到您,如文中有不对之处,还请批评指正。


参考资料:
IEnumerable 接口
C#概念 - 迭代器
C#编程指南 - 迭代器
yield 语句 - 提供下一个元素
IEnumerable和IEnumerator 详解
C#内建接口:IEnumerable
[C#.NET 拾遗补漏] 理解 yield 关键字
C# 中的yield return机制和原理
深入理解C#中的yield关键字:提升迭代性能与效率
由C# yield return引发的思考
[C#.NET 拾遗补漏] 理解 yield 关键字

相关推荐
小吴同学·1 小时前
.NET6 WebApi第1讲:VSCode开发.NET项目、区别.NET5框架【两个框架启动流程详解】
c#·.netcore·.net core
Python私教1 小时前
model中能定义字段声明不存储到数据库吗
数据库·oracle
吾日三省吾码2 小时前
JVM 性能调优
java
弗拉唐3 小时前
springBoot,mp,ssm整合案例
java·spring boot·mybatis
oi774 小时前
使用itextpdf进行pdf模版填充中文文本时部分字不显示问题
java·服务器
BestandW1shEs4 小时前
谈谈Mysql的常见基础问题
数据库·mysql
重生之Java开发工程师4 小时前
MySQL中的CAST类型转换函数
数据库·sql·mysql
教练、我想打篮球4 小时前
66 mysql 的 表自增长锁
数据库·mysql
Ljw...4 小时前
表的操作(MySQL)
数据库·mysql·表的操作