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

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

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


当前回答

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

其他回答

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

简单的演示,了解产量

using System;
using System.Collections.Generic;
using System.Linq;

namespace ConsoleApp_demo_yield {
    class Program
    {
        static void Main(string[] args)
        {
            var letters = new List<string>() { "a1", "b1", "c2", "d2" };

            // Not yield
            var test1 = GetNotYield(letters);

            foreach (var t in test1)
            {
                Console.WriteLine(t);
            }

            // yield
            var test2 = GetWithYield(letters).ToList();

            foreach (var t in test2)
            {
                Console.WriteLine(t);
            }

            Console.ReadKey();
        }

        private static IList<string> GetNotYield(IList<string> list)
        {
            var temp = new List<string>();
            foreach(var x in list)
            {
                
                if (x.Contains("2")) { 
                temp.Add(x);
                }
            }

            return temp;
        }

        private static IEnumerable<string> GetWithYield(IList<string> list)
        {
            foreach (var x in list)
            {
                if (x.Contains("2"))
                {
                    yield return x;
                }
            }
        }
    } 
}

屈服有两大用处,

它有助于在不创建临时集合的情况下提供自定义迭代。 它有助于进行有状态迭代。

为了更清楚地解释以上两点,我制作了一个简单的视频,你可以在这里观看

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

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

它试图带来一些Ruby的善良:) 概念:这是一些示例Ruby代码,打印出数组的每个元素

 rubyArray = [1,2,3,4,5,6,7,8,9,10]
    rubyArray.each{|x| 
        puts x   # do whatever with x
    }

数组的每个方法实现将控制权移交给调用者('puts x'),数组的每个元素都整齐地表示为x。然后调用者可以对x做任何需要做的事情。

然而。net并没有完全做到这一点。c#似乎已经与IEnumerable耦合了yield,在某种程度上迫使你在调用者中写一个foreach循环,就像Mendelt的响应中看到的那样。没有那么优雅。

//calling code
foreach(int i in obCustomClass.Each())
{
    Console.WriteLine(i.ToString());
}

// CustomClass implementation
private int[] data = {1,2,3,4,5,6,7,8,9,10};
public IEnumerable<int> Each()
{
   for(int iLooper=0; iLooper<data.Length; ++iLooper)
        yield return data[iLooper]; 
}