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

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

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


当前回答

yield上下文关键字实际上在这里做了很多事情。

该函数返回一个实现IEnumerable<object>接口的对象。如果调用函数开始覆盖该对象,则再次调用该函数,直到它“屈服”。这是c# 2.0中引入的语法糖。在早期的版本中,你必须创建你自己的IEnumerable和IEnumerator对象来做这样的事情。

理解这类代码的最简单方法是输入一个示例,设置一些断点,然后看看会发生什么。试试下面这个例子:

public void Consumer()
{
    foreach(int i in Integers())
    {
        Console.WriteLine(i.ToString());
    }
}

public IEnumerable<int> Integers()
{
    yield return 1;
    yield return 2;
    yield return 4;
    yield return 8;
    yield return 16;
    yield return 16777216;
}

当您逐步浏览这个示例时,您将发现对Integers()的第一个调用返回1。第二次调用返回2,并且不再执行yield return 1行。

下面是一个现实生活中的例子:

public IEnumerable<T> Read<T>(string sql, Func<IDataReader, T> make, params object[] parms)
{
    using (var connection = CreateConnection())
    {
        using (var command = CreateCommand(CommandType.Text, sql, connection, parms))
        {
            command.CommandTimeout = dataBaseSettings.ReadCommandTimeout;
            using (var reader = command.ExecuteReader())
            {
                while (reader.Read())
                {
                    yield return make(reader);
                }
            }
        }
    }
}

其他回答

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

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

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

列表或数组实现立即加载所有项,而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:创建联系人 你好,鲍勃

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

简单的演示,了解产量

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

yield上下文关键字实际上在这里做了很多事情。

该函数返回一个实现IEnumerable<object>接口的对象。如果调用函数开始覆盖该对象,则再次调用该函数,直到它“屈服”。这是c# 2.0中引入的语法糖。在早期的版本中,你必须创建你自己的IEnumerable和IEnumerator对象来做这样的事情。

理解这类代码的最简单方法是输入一个示例,设置一些断点,然后看看会发生什么。试试下面这个例子:

public void Consumer()
{
    foreach(int i in Integers())
    {
        Console.WriteLine(i.ToString());
    }
}

public IEnumerable<int> Integers()
{
    yield return 1;
    yield return 2;
    yield return 4;
    yield return 8;
    yield return 16;
    yield return 16777216;
}

当您逐步浏览这个示例时,您将发现对Integers()的第一个调用返回1。第二次调用返回2,并且不再执行yield return 1行。

下面是一个现实生活中的例子:

public IEnumerable<T> Read<T>(string sql, Func<IDataReader, T> make, params object[] parms)
{
    using (var connection = CreateConnection())
    {
        using (var command = CreateCommand(CommandType.Text, sql, connection, parms))
        {
            command.CommandTimeout = dataBaseSettings.ReadCommandTimeout;
            using (var reader = command.ExecuteReader())
            {
                while (reader.Read())
                {
                    yield return make(reader);
                }
            }
        }
    }
}

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