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#教程。
