关于.NET:在C#语言中理解yield有困难

Trouble understanding yield in C#

本问题已经有最佳答案,请猛点这里访问。

我希望能对我最近在调试器中所经历的一个片段进行一些澄清,但仅仅是无法真正理解。

我正在进行一个关于复数的C课程,当前的主题是关于yield,返回带有关键字的IEnumerable

我有一个非常基本的函数,它返回VendorsIEnumerable集合(一个带有IdCompanyNameEmail的简单类):

1
2
3
4
5
6
7
8
9
public IEnumerable<Vendor> RetrieveWithIterator()
{
    this.Retrieve(); // <-- I've got a breakpoint here
    foreach(var vendor in _vendors)
    {
        Debug.WriteLine($"Vendor Id: {vendor.VendorId}");
        yield return vendor;
    }
}

我在单元测试中得到了这个代码,我用它来测试函数:

1
2
3
4
5
6
var vendorIterator = repository.RetrieveWithIterator(); // <-- Why don't it enter function?
foreach (var item in vendorIterator) // <-- But starts here?
{
    Debug.WriteLine(item);
}
var actual = vendorIterator.ToList();

我真的不明白,而且我相信很多初学者也有同样的问题,这就是为什么对EDOCX1的初始调用(7)不启动函数,而是在我们开始迭代返回的IEnumerable集合时开始的(见注释)。


这被称为延迟执行,yield很懒惰,只会按需要工作。

这有很多优点,其中之一就是您可以创建看似无限的枚举:

1
2
3
4
5
public IEnumerable<int> InfiniteOnes()
{
     while (true)
         yield 1;
}

现在想象一下:

1
var infiniteOnes = InfiniteOnes();

如果执行得很热情,你会得到一个StackOverflow例外,你会非常高兴的。

另一方面,由于它的懒惰,您可以执行以下操作:

1
2
3
var infiniteOnes = InfiniteOnes();
//.... some code
foreach (var one in infiniteOnes.Take(100)) { ... }

后来,

1
foreach (var one in infiniteOnes.Take(10000)) { ... }

迭代器块只在需要时运行;当枚举被迭代时,不在之前,不在之后。


来自MSDN:

推迟执行

延迟执行意味着表达式的计算被延迟,直到实际需要它的实现值为止。当必须操作大型数据集合时,延迟执行可以极大地提高性能,特别是在包含一系列链接查询或操作的程序中。在最好的情况下,延迟执行只允许通过源集合进行单个迭代。

当在迭代器块中使用时,yield关键字(以yield return语句的形式)直接在C语言中支持延迟执行。这样的迭代器必须返回类型IEnumeratorIEnumerator的集合(或派生类型)。

1
2
3
4
5
6
var vendorIterator = repository.RetrieveWithIterator(); // <-- Lets deferred the execution
foreach (var item in vendorIterator) // <-- execute it because we need it
{
    Debug.WriteLine(item);
}
var actual = vendorIterator.ToList();

渴望与懒惰的评价

当您编写一个实现延迟执行的方法时,您还必须决定是使用Lazy Evaluation实现该方法,还是使用热切的Evaluation实现该方法。

  • 在惰性计算中,每次调用迭代器时都会处理源集合的单个元素。这是实现迭代器的典型方式。
  • 在EAGER评估中,对迭代器的第一个调用将导致整个集合被处理。可能还需要源集合的临时副本。

延迟评估通常会产生更好的性能,因为它在整个集合评估过程中平均分配开销处理,并最小化临时数据的使用。当然,对于某些操作,除了实现中间结果之外没有其他选择。

来源


当需要时,它将在您循环它们时获取这些项。这样说,你只需要前4个结果,然后你打破,它不会产生更多的东西,你只是节省了一些处理能力!

从MS博士:

You use a yield return statement to return each element one at a time.
You consume an iterator method by using a foreach statement or LINQ query. Each iteration of the foreach loop calls the iterator method. When a yield return statement is reached in the iterator method, expression is returned, and the current location in code is retained. Execution is restarted from that location the next time that the iterator function is called.
You can use a yield break statement to end the iteration.

注意-如果您在生成方法的结果上执行cx1(2),它将像返回单个列表一样工作,从而破坏了生成的目的。