在如何只能暴露IList<>的一个片段问题中,其中一个答案有以下代码片段:

IEnumerable<object> FilteredList()
{
    foreach(object item in FullList)
    {
        if(IsItemInPartialList(item))
            yield return item;
    }
}

yield关键字在这里做什么?我在一些地方看到过它,还有一个问题,但我还没搞清楚它到底是干什么的。我习惯于从一个线程屈服于另一个线程的意义上考虑屈服,但这在这里似乎无关紧要。


当前回答

直观地,关键字从函数中返回一个值而不离开它,即在您的代码示例中,它返回当前项的值,然后继续循环。更正式地说,它被编译器用来为迭代器生成代码。迭代器是返回IEnumerable对象的函数。MSDN上有几篇关于它们的文章。

其他回答

它产生了可枚举的序列。它所做的实际上是创建本地IEnumerable序列,并将其作为方法结果返回

yield关键字允许您在迭代器块上以该形式创建一个IEnumerable<T>。这个迭代器块支持延迟执行,如果你不熟悉这个概念,它可能看起来很神奇。然而,在一天结束的时候,它只是没有任何奇怪的技巧执行的代码。

迭代器块可以被描述为语法糖,其中编译器生成一个状态机,跟踪枚举对象的枚举进行了多远。要枚举一个可枚举对象,通常使用foreach循环。然而,foreach循环也是语法糖。所以你是两个从实际代码中移除的抽象,这就是为什么最初可能很难理解它们是如何一起工作的。

假设你有一个非常简单的迭代器块:

IEnumerable<int> IteratorBlock()
{
    Console.WriteLine("Begin");
    yield return 1;
    Console.WriteLine("After 1");
    yield return 2;
    Console.WriteLine("After 2");
    yield return 42;
    Console.WriteLine("End");
}

真正的迭代器块通常有条件和循环,但当你检查条件并展开循环时,它们仍然是yield语句与其他代码交织在一起。

要枚举迭代器块,使用foreach循环:

foreach (var i in IteratorBlock())
    Console.WriteLine(i);

下面是输出(这里没有惊喜):

Begin
1
After 1
2
After 2
42
End

如上所述,foreach是语法糖:

IEnumerator<int> enumerator = null;
try
{
    enumerator = IteratorBlock().GetEnumerator();
    while (enumerator.MoveNext())
    {
        var i = enumerator.Current;
        Console.WriteLine(i);
    }
}
finally
{
    enumerator?.Dispose();
}

为了解决这个问题,我画了一个去掉抽象的序列图:

编译器生成的状态机也实现了枚举器,但为了使图更清晰,我将它们作为单独的实例显示。(当状态机从另一个线程中枚举时,您实际上会得到单独的实例,但在这里这个细节并不重要。)

每次调用迭代器块时,都会创建一个状态机的新实例。但是,迭代器块中的任何代码都不会执行,直到enumerator.MoveNext()第一次执行。这就是延迟执行的工作方式。这里有一个(相当愚蠢的)例子:

var evenNumbers = IteratorBlock().Where(i => i%2 == 0);

此时迭代器还没有执行。Where子句创建了一个新的IEnumerable<T>,它包装了IteratorBlock返回的IEnumerable<T>,但是这个可枚举对象还没有被枚举。这发生在你执行foreach循环时:

foreach (var evenNumber in evenNumbers)
    Console.WriteLine(eventNumber);

如果枚举可枚举对象两次,那么每次都会创建一个状态机的新实例,迭代器块将执行相同的代码两次。

Notice that LINQ methods like ToList(), ToArray(), First(), Count() etc. will use a foreach loop to enumerate the enumerable. For instance ToList() will enumerate all elements of the enumerable and store them in a list. You can now access the list to get all elements of the enumerable without the iterator block executing again. There is a trade-off between using CPU to produce the elements of the enumerable multiple times and memory to store the elements of the enumeration to access them multiple times when using methods like ToList().

现在你可以在异步流中使用yield关键字。

c# 8.0引入了异步流,它对流数据源进行了建模。数据流通常异步检索或生成元素。异步流依赖于。net Standard 2.1中引入的新接口。. net Core 3.0及更高版本支持这些接口。它们为异步流数据源提供了自然的编程模型。 来源:微软文档

在下面的例子

using System;
using System.Collections.Generic;               
using System.Threading.Tasks;

public class Program
{
    public static async Task Main()
    {
        List<int> numbers = new List<int>() { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
        
        await foreach(int number in YieldReturnNumbers(numbers))
        {
            Console.WriteLine(number);
        }
    }
    
    public static async IAsyncEnumerable<int> YieldReturnNumbers(List<int> numbers) 
    {
        foreach (int number in numbers)
        {
            await Task.Delay(1000);
            yield return number;
        }
    }
}

c#的yield关键字,简单地说,允许多次调用一段被称为迭代器的代码,它知道如何在完成之前返回,并且当再次调用时,继续它离开的地方——也就是说,它帮助迭代器在连续调用中返回的序列中的每一项都具有透明的状态。

在JavaScript中,同样的概念被称为生成器。

最近Raymond Chen也发表了一系列关于yield关键字的有趣文章。

c#中迭代器的实现及其后果(第1部分) c#中迭代器的实现及其后果(第2部分) c#中迭代器的实现及其后果(第3部分) c#中迭代器的实现及其后果(第4部分)

虽然它名义上用于轻松实现迭代器模式,但可以泛化为状态机。没有必要引用Raymond,最后一部分也链接到其他用法(但Entin博客中的例子特别好,展示了如何编写异步安全代码)。