C# yield
最后修改时间:2024 年 2 月 1 日
在本文中,我们将展示如何在 C# 语言中使用 yield 关键字。
yield 关键字
yield 关键字用于对集合进行自定义的有状态迭代。 yield 关键字告诉编译器,包含它的方法是一个迭代器块。
yield return <expression>; yield break;
yield return 语句一次返回一个元素。 yield 关键字的返回类型是 IEnumerable 或 IEnumerator。 yield break 语句用于结束迭代。
我们可以使用 foreach 循环或 LINQ 查询来使用包含 yield return 语句的迭代器方法。 循环的每次迭代都会调用迭代器方法。 当在迭代器方法中遇到 yield return 语句时,表达式将被返回,并且代码中的当前位置将被保留。 下次调用迭代器函数时,将从该位置重新开始执行。
使用 yield 的两个重要方面是
- 延迟计算
- 延迟执行
C# yield 示例
在第一个例子中,我们使用斐波那契数列。
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...
斐波那契数列是一个数字序列,其中下一个数字是通过将它之前的两个数字相加得到的。
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 关键字来生成斐波那契数列。
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 语句。 n1、n2、temp 值会被记住; C# 在后台创建一个类来保存这些值。
我们可以有多个 yield 语句。
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 存储状态; 下一个程序演示了这一点。
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 分区示例
在下一个示例中,我们将比较两种对大型列表进行分区的效率的方法。
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。
来源
在本文中,我们使用了 C# yield 关键字。
作者
列出所有 C# 教程。