C#yield教程展示了如何在C#语言中使用yield关键字。
yield关键字
yield
关键字用于对集合进行自定义有状态迭代。yield关键字告诉编译器它出现的方法是一个迭代器块。
yield return <expression>; yield break;
yieldreturn
语句一次返回一个元素。yield关键字的返回类型是IEnumerable
或IEnumerator
。yieldbreak
语句用于结束迭代。
我们可以通过使用foreach循环或LINQ查询来使用包含yieldreturn语句的迭代器方法。循环的每次迭代都调用迭代器方法。当在迭代器方法中到达yieldreturn语句时,返回表达式,并保留代码中的当前位置。下次调用迭代器函数时,将从该位置重新开始执行。
使用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; }
yieldreturn
将当前计算的值返回给上面的foreach
语句。n1
、n2
、temp
值被记住;C#在后台创建一个类来保存这些值。
C#产量累计
yield
存储状态;下一个程序演示了这一点。
var vals = new List<int> { 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.7GB与30MB。
在本文中,我们使用了C#yield
关键字。
访问C#教程或列出所有C#教程。