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

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

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


当前回答

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

其他回答

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的角度来表达这一点。

这是一个。 如果你需要另一个,请再打电话。 我会记得我给你的。 你下次打电话来的时候我才知道能不能再给你。

乍一看,yield return是一个。net糖返回一个IEnumerable。

如果没有yield,集合中的所有项都是一次性创建的:

class SomeData
{
    public SomeData() { }

    static public IEnumerable<SomeData> CreateSomeDatas()
    {
        return new List<SomeData> {
            new SomeData(), 
            new SomeData(), 
            new SomeData()
        };
    }
}

同样的代码使用yield,它逐项返回:

class SomeData
{
    public SomeData() { }

    static public IEnumerable<SomeData> CreateSomeDatas()
    {
        yield return new SomeData();
        yield return new SomeData();
        yield return new SomeData();
    }
}

使用yield的优点是,如果使用数据的函数只需要集合的第一个项,则不会创建其余的项。

yield操作符允许根据需要创建项。这是一个使用它的好理由。

迭代。它创建了一个“隐藏的”状态机,它会记住您在函数的每个额外循环中的位置,并从那里开始处理。

这是一个非常简单和容易的方法来创建一个枚举对象。编译器创建了一个类来包装您的方法,并在本例中实现IEnumerable<object>。如果没有yield关键字,则必须创建一个实现IEnumerable<object>的对象。