无法理解 C# 中的 yield

Trouble understanding yield in C#

我希望得到一些关于我最近在调试器中逐步执行的片段的说明,但我无法真正理解。

我正在 PluralSight 上学习 C# 课程,当前主题是 yield 并返回 IEnumerable<T> 与关键字。

我有这个过于基本的功能,returns IEnumerable Vendors 的集合(一个简单的 class 和 IdCompanyNameEmail):

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;
    }
}

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

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();

我真的不明白,我相信很多初学者都有同样的问题,就是为什么最初调用 RetrieveWithIterator 没有启动函数,但是它当我们开始遍历其返回的 IEnumerable 集合时,rather 开始(参见评论)。

这叫做延迟执行,yield 是懒惰的,只在需要的时候工作。

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

public IEnumerable<int> InfiniteOnes()
{
     while (true)
         yield 1;
}

现在想象一下:

var infiniteOnes = InfiniteOnes();

会急切地执行,你会非常愉快地遇到 Whosebug 异常。

另一方面,因为懒惰,你可以这样做:

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

后来,

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

迭代器块 运行 仅在需要时才使用;枚举迭代时,不是之前,不是之后。

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

来自 MS Docs:

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.

注意 - 如果你 .ToList() 一个方法的结果,它会像你返回一个列表一样工作,从而违背了 yield 的目的.

来自 msdn:

延迟执行

延迟执行意味着表达式的计算被延迟,直到实际需要它的实现值。当您必须操作大型数据集合时,延迟执行可以极大地提高性能,尤其是在包含一系列链式查询或操作的程序中。在最好的情况下,延迟执行仅支持对源集合进行一次迭代。

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

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();

热切与懒惰评估

当您编写实现延迟执行的方法时,您还必须决定是使用惰性求值还是急切求值来实现该方法。

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

惰性求值通常会产生更好的性能,因为它在整个集合求值过程中平均分配开销处理,并最大限度地减少临时数据的使用。当然,对于某些操作,除了具体化中间结果之外别无选择。

source