在如何只能暴露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<object>。如果没有yield关键字,则必须创建一个实现IEnumerable<object>的对象。

列表或数组实现立即加载所有项,而yield实现提供了延迟执行的解决方案。

在实践中,为了减少应用程序的资源消耗,通常需要执行所需的最小工作量。

例如,我们可能有一个处理数据库中数百万条记录的应用程序。当我们在一个基于延迟执行的拉式模型中使用IEnumerable时,可以实现以下好处:

Scalability, reliability and predictability are likely to improve since the number of records does not significantly affect the application’s resource requirements. Performance and responsiveness are likely to improve since processing can start immediately instead of waiting for the entire collection to be loaded first. Recoverability and utilisation are likely to improve since the application can be stopped, started, interrupted or fail. Only the items in progress will be lost compared to pre-fetching all of the data where only using a portion of the results was actually used. Continuous processing is possible in environments where constant workload streams are added.

这里是先构建集合(如列表)与使用yield之间的比较。

列表的例子

    public class ContactListStore : IStore<ContactModel>
    {
        public IEnumerable<ContactModel> GetEnumerator()
        {
            var contacts = new List<ContactModel>();
            Console.WriteLine("ContactListStore: Creating contact 1");
            contacts.Add(new ContactModel() { FirstName = "Bob", LastName = "Blue" });
            Console.WriteLine("ContactListStore: Creating contact 2");
            contacts.Add(new ContactModel() { FirstName = "Jim", LastName = "Green" });
            Console.WriteLine("ContactListStore: Creating contact 3");
            contacts.Add(new ContactModel() { FirstName = "Susan", LastName = "Orange" });
            return contacts;
        }
    }

    static void Main(string[] args)
    {
        var store = new ContactListStore();
        var contacts = store.GetEnumerator();

        Console.WriteLine("Ready to iterate through the collection.");
        Console.ReadLine();
    }

控制台输出 ContactListStore:创建联系人 ContactListStore:创建联系人 ContactListStore:正在创建联系人 准备遍历集合。

注意:整个集合被加载到内存中,甚至没有请求列表中的任何项

收益率的例子

public class ContactYieldStore : IStore<ContactModel>
{
    public IEnumerable<ContactModel> GetEnumerator()
    {
        Console.WriteLine("ContactYieldStore: Creating contact 1");
        yield return new ContactModel() { FirstName = "Bob", LastName = "Blue" };
        Console.WriteLine("ContactYieldStore: Creating contact 2");
        yield return new ContactModel() { FirstName = "Jim", LastName = "Green" };
        Console.WriteLine("ContactYieldStore: Creating contact 3");
        yield return new ContactModel() { FirstName = "Susan", LastName = "Orange" };
    }
}

static void Main(string[] args)
{
    var store = new ContactYieldStore();
    var contacts = store.GetEnumerator();

    Console.WriteLine("Ready to iterate through the collection.");
    Console.ReadLine();
}

控制台输出 准备遍历集合。

注意:收集根本没有执行。这是由于IEnumerable的“延迟执行”特性。只有在真正需要时才会构造一个项。

让我们再次调用集合,并在取回集合中的第一个联系人时反转行为。

static void Main(string[] args)
{
    var store = new ContactYieldStore();
    var contacts = store.GetEnumerator();
    Console.WriteLine("Ready to iterate through the collection");
    Console.WriteLine("Hello {0}", contacts.First().FirstName);
    Console.ReadLine();
}

控制台输出 准备遍历集合 ContactYieldStore:创建联系人 你好,鲍勃

好了!当客户端从集合中“拉出”项目时,只构造了第一个联系人。

现在你可以在异步流中使用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;
        }
    }
}

乍一看,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操作符允许根据需要创建项。这是一个使用它的好理由。