开放的编程资料库

当前位置:我爱分享网 > C#教程 > 正文

C# 产量

C#yield教程展示了如何在C#语言中使用yield关键字。

yield关键字

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

yield return <expression>;
yield break;

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

我们可以通过使用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语句。n1n2temp值被记住;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#教程。

未经允许不得转载:我爱分享网 » C# 产量

感觉很棒!可以赞赏支持我哟~

赞(0) 打赏