ZetCode

C# yield

最后修改时间:2024 年 2 月 1 日

在本文中,我们将展示如何在 C# 语言中使用 yield 关键字。

yield 关键字

yield 关键字用于对集合进行自定义的有状态迭代。 yield 关键字告诉编译器,包含它的方法是一个迭代器块。

yield return <expression>;
yield break;

yield return 语句一次返回一个元素。 yield 关键字的返回类型是 IEnumerableIEnumeratoryield break 语句用于结束迭代。

我们可以使用 foreach 循环或 LINQ 查询来使用包含 yield return 语句的迭代器方法。 循环的每次迭代都会调用迭代器方法。 当在迭代器方法中遇到 yield return 语句时,表达式将被返回,并且代码中的当前位置将被保留。 下次调用迭代器函数时,将从该位置重新开始执行。

使用 yield 的两个重要方面是

C# yield 示例

在第一个例子中,我们使用斐波那契数列。

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...

斐波那契数列是一个数字序列,其中下一个数字是通过将它之前的两个数字相加得到的。

Program.cs
var data = Fibonacci(10);

foreach (int e in data)
{
    Console.WriteLine(e);
}

IEnumerable<int> Fibonacci(int n)
{
    var vals = new List<int>();

    for (int i = 0, n1 = 0, n2 = 1; i < n; i++)
    {
        int fib = n1 + n2;
     
        n1 = n2;

        vals.Add(fib);
        n2 = fib;
    }

    return vals;
}

在这里,我们在没有 yield 关键字的情况下计算序列。 我们打印序列的前十个值。

var vals = new List<int>();

此实现需要一个新列表。 想象一下,我们处理了数亿个值。 这将大大降低我们的计算速度,并且需要大量的内存。

$ dotnet run 
1
2
3
5
8
13
21
34
55
89

接下来,我们使用 yield 关键字来生成斐波那契数列。

Program.cs
foreach (int fib in Fibonacci(10))
{
    Console.WriteLine(fib);
}

IEnumerable<int> Fibonacci(int n)
{
    for (int i = 0, n1 = 0, n2 = 1; i < n; i++)
    {
        yield return n1;

        int temp = n1 + n2;
        n1 = n2;

        n2 = temp;
    }
}

此实现会在达到序列的指定结尾之前开始生成数字。

for (int i = 0, n1 = 0, n2 = 1; i < n; i++)
{
    yield return n1;

    int temp = n1 + n2;
    n1 = n2;

    n2 = temp;
}

yield return 将当前计算的值返回到上面的 foreach 语句。 n1n2temp 值会被记住; C# 在后台创建一个类来保存这些值。


我们可以有多个 yield 语句。

Program.cs
int n = 10;

IEnumerable<string> res = FibSeq().TakeWhile(f => f.n <= n).Select(f => $"{f.fib}");

Console.WriteLine(string.Join(" ", res));

IEnumerable<(int n, int fib)> FibSeq()
{
    yield return (0, 0);
    yield return (1, 1);

    var (x, y, n) = (1, 0, 0);

    while (x < int.MaxValue - y)
    {
        (x, y, n) = (x + y, x, n + 1);
        yield return (n, x);
    }
}

在该示例中,我们借助元组计算斐波那契数列。

IEnumerable<string> res = FibSeq().TakeWhile(f => f.n <= n).Select(f => $"{f.fib}");

我们使用 LINQ 的 TakeWhile 方法来使用斐波那契数列。

Console.WriteLine(string.Join(" ", res));

字符串序列被连接起来。

IEnumerable<(int n, int fib)> FibSeq()
{
    yield return (0, 0);
    yield return (1, 1);

    var (x, y, n) = (1, 0, 0);

    while (x < int.MaxValue - y)
    {
        (x, y, n) = (x + y, x, n + 1);
        yield return (n, x);
    }
}

FibSeq 方法返回一个元组值序列。 每个元组包含 n 值,即我们生成序列的上限,以及当前的斐波那契值 fib

yield return (0, 0);
yield return (1, 1);

前两个元组使用 yield return 返回。

var (x, y, n) = (1, 0, 0);

while (x < int.MaxValue - y)
{
    (x, y, n) = (x + y, x, n + 1);
    yield return (n, x);
}

序列的其余部分使用 while 循环计算。 该序列一直到 int.MaxValue

C# yield 运行总计

yield 存储状态; 下一个程序演示了这一点。

Program.cs
List<int> vals = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

foreach (int e in RunningTotal())
{
    Console.WriteLine(e);
}

IEnumerable<int> RunningTotal()
{
    int runningTotal = 0;

    foreach (int val in vals)
    {
        runningTotal += val;
        yield return runningTotal;
    }
}

该示例计算整数列表的运行总计。 当控制在迭代器和迭代器的使用者之间传递时,runningTotal 会被存储。

$ dotnet run 
1
3
6
10
15
21
28
36
45
55

C# yield 分区示例

在下一个示例中,我们将比较两种对大型列表进行分区的效率的方法。

Program.cs
using System.Collections.ObjectModel;

var vals = Enumerable.Range(1, 100_000_000);

var option = int.Parse(args[0]);

IEnumerable<IEnumerable<int>> result;

if (option == 1)
{
    result = Partition1(vals, 5);
} else 
{
    result = Partition2(vals, 5);
}

foreach (var part in result)
{
    // Console.WriteLine(string.Join(", ", part));
}

Console.WriteLine(string.Join(", ", result.First()));
Console.WriteLine(string.Join(", ", result.Last()));

Console.WriteLine("-------------------");
Console.WriteLine("Finished");

IEnumerable<IEnumerable<int>> Partition1(IEnumerable<int> source, int size)
{
    int[] array = null;
    int count = 0;

    var data = new List<IEnumerable<int>>();

    foreach (int item in source)
    {
        if (array == null)
        {
            array = new int[size];
        }

        array[count] = item;
        count++;

        if (count == size)
        {
            data.Add(new ReadOnlyCollection<int>(array));
            array = null;
            count = 0;
        }
    }

    if (array != null)
    {
        Array.Resize(ref array, count);
        data.Add(new ReadOnlyCollection<int>(array));
    }

    return data;
}

IEnumerable<IEnumerable<int>> Partition2(IEnumerable<int> source, int size)
{
    int[] array = null;
    int count = 0;

    foreach (int item in source)
    {
        if (array == null)
        {
            array = new int[size];
        }

        array[count] = item;
        count++;

        if (count == size)
        {
            yield return new ReadOnlyCollection<int>(array);
            array = null;
            count = 0;
        }
    }

    if (array != null)
    {
        Array.Resize(ref array, count);
        yield return new ReadOnlyCollection<int>(array);
    }
}

我们有一个亿个值的序列。 我们使用和不使用 yield 关键字将它们分成五组值,并比较效率。

var vals = Enumerable.Range(1, 100_000_000);

使用 Enumerable.Range 生成一个亿个值的序列。

var option = int.Parse(args[0]);

IEnumerable<IEnumerable<int>> result;

if (option == 1)
{
    result = Partition1(vals, 5);
} else 
{
    result = Partition2(vals, 5);
}

该程序使用参数运行。 选项 1 调用 Partition1 函数。 yield 关键字在 Partition2 中使用,并使用 1 以外的选项调用。

var data = new List<IEnumerable<int>>();
...
return data;

Partition1 函数构建一个包含内部划分值的列表。 对于一亿个值,这需要大量的内存。 此外,如果可用内存不足,操作系统将开始将内存交换到磁盘,从而降低计算速度。

if (array != null)
{
    Array.Resize(ref array, count);
    yield return new ReadOnlyCollection<int>(array);
}

Partition2 中,我们一次返回一个分区集合。 我们不会等待整个过程完成。 这种方法需要的内存更少。

$ /usr/bin/time -f "%M KB %e s" bin/Release/net5.0/Partition 1
1, 2, 3, 4, 5
99999996, 99999997, 99999998, 99999999, 100000000
-------------------
Finished
1696712 KB 6.38 s

$ /usr/bin/time -f "%M KB %e s" bin/Release/net5.0/Partition 2
1, 2, 3, 4, 5
99999996, 99999997, 99999998, 99999999, 100000000
-------------------
Finished
30388 KB 2.99 s

我们使用 time 命令来比较这两个函数。 在我们的例子中,它是 1.7 GB 与 30 MB。

来源

yield 语句 - 语言参考

在本文中,我们使用了 C# yield 关键字。

作者

我的名字是 Jan Bodnar,我是一位充满热情的程序员,拥有丰富的编程经验。 自 2007 年以来,我一直在撰写编程文章。 迄今为止,我已经撰写了超过 1,400 篇文章和 8 本电子书。 我拥有超过十年的编程教学经验。

列出所有 C# 教程