C#中yield关键字的作用是什么?

1035

如何仅公开 IList <> 的片段问题中,其中一个答案包含以下代码片段:

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

yield关键字在这里有什么作用?我在一些地方看到它被提到,还有一个问题,但我没有完全弄清楚它实际上是做什么的。我习惯于将yield视为一个线程让给另一个线程,但这似乎与此处无关。


1
这里只是有关它的 MSDN 链接 http://msdn.microsoft.com/en-us/library/vstudio/9k7k7cf0.aspx - NoWar
25
这并不令人意外。混淆源于我们习惯将“return”视为函数输出,但当它前面带有“yield”时,它并不是这样。 - Larry
20个回答

934

yield 上下文关键字在这里实际上起到了很多作用。

该函数返回一个实现了 IEnumerable<object> 接口的对象。如果调用函数开始对此对象进行 foreach 循环,那么函数会再次被调用,直到它“yield”(产出)。这是在 C# 2.0 中引入的语法糖。在早期版本中,您必须创建自己的 IEnumerableIEnumerator 对象来执行此类操作。

理解这种代码的最简单方法是输入一个示例,设置一些断点并查看发生了什么。尝试逐步执行此示例:

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

126
在这种情况下,这会更容易,我只是在这里使用整数来展示yield return的工作方式。使用yield return的好处是它是实现迭代器模式的一种非常快速的方法,因此可以进行惰性评估。 - Mendelt
136
值得注意的是,当你不想再返回任何项时,可以使用yield break; - Rory
11
yield is not a keyword. If it were then I could not use yield as an identifier as in int yield = 500; - Brandin
15
@Brandin,这是因为所有编程语言都支持两种类型的关键字,即保留和上下文。yield 属于后一类,这就是为什么你的代码不会被 C# 编译器禁止的原因。更多详细信息请参见此处:https://ericlippert.com/2009/05/11/reserved-and-contextual-keywords/ 你会很高兴知道,有些保留字在语言中并不被认为是关键字,例如 Java 中的 goto。更多详情请参见此处:https://dev59.com/LHE85IYBdhLWcg3w64IA - RBT
10
如果一个调用函数开始对这个对象进行 foreach 循环,直到它“yield”为止,该函数将被再次调用。这句话听起来不太对。我一直认为在C#中,"yield"关键字是指“作物收获丰盛”,而不是“汽车让行于行人”。 - Zack
显示剩余3条评论

436

迭代。它在幕后创建了一个状态机,记住每次函数调用的位置,并从那里继续执行。


287

yield有两个重要的用途:

  1. 它可以提供定制的迭代功能而无需创建临时集合。

  2. 它可以实现具有状态的迭代。 enter image description here

为了更加形象化地解释上述两点,我制作了一个简单的视频,您可以在这里观看。


26
这个视频帮助我清楚地理解了“yield”。@ShivprasadKoirala的代码项目文章什么是C# Yield?也是一个很好的参考来源。 - Dush
1
我还想补充第三个观点:yield是创建自定义IEnumerator的“快速”方法(而不是使一个类实现IEnumerator接口)。 - MrTourkos
5
好的视频,但我想知道... 使用yield实现显然更清晰,但它必须在内部创建自己的临时内存或/和列表以便跟踪状态(或者创建状态机)。那么,“Yield”除了使实现更简单、更美观外,还有其他作用吗?关于效率方面,使用Yield运行代码比不使用Yield更高效/快速吗? - toughQuestions

155

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

虽然 yield 关键字通常用于轻松实现迭代器模式,但其可以泛化为状态机。不必引用 Raymond 的话,最后一部分还链接到其他用途(但 Entin 博客中的例子尤其出色,展示了如何编写异步安全代码)。


1
需要点赞。他很好地解释了运算符和内部机制的目的。 - sajidnizami
4
第一部分解释了"yield return"的语法糖。讲解得很好! - dror

147
乍一看,yield return 是一个用于返回 IEnumerable 的 .NET 语法糖。
没有使用 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操作符可以按需创建项目。这是使用它的一个好理由。

71

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

在实践中,通常希望尽可能少地执行工作,以减少应用程序的资源消耗。

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

  • 可扩展性、可靠性和可预测性可能会提高,因为记录的数量不会显著影响应用程序的资源需求。
  • 性能和响应性可能会提高,因为处理可以立即开始,而不是等待整个集合首先加载完毕。
  • 可恢复性和利用率可能会提高,因为应用程序可以停止、启动、中断或失败。与预取所有数据相比,只有正在进行的项目将丢失,而实际上只使用了部分结果。
  • 连续处理在添加恒定工作负载流的环境中是可能的。

下面是建立集合和使用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: 创建联系人1
ContactListStore: 创建联系人2
ContactListStore: 创建联系人3
准备遍历整个集合。

注意:整个集合在没有请求列表中的任何一项情况下被加载到内存中。

yield示例

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: 创建联系人1
Hello Bob

太好了!当客户端从集合中“拉出”该项时,只有第一个联系人被构建。


3
这个回答需要更多的关注!谢谢。 - leon22
@leon22 绝对 +2 - Soner from The Ottoman Empire
然而,这会带来一定的性能损失。对于小型内存列表使用yield几乎没有意义。 - cskwg

48

yield return 用于枚举器。在每次调用 yield 语句时,控制权都会返回给调用者,但它确保了被调用方法的状态得以维护。由于这一点,当调用者枚举下一个元素时,它会从 yield 语句后面紧接着的语句继续执行被调用方法。

让我们通过一个例子来理解这个概念。在这个例子中,我列出了每行代码执行的顺序。

static void Main(string[] args)
{
    foreach (int fib in Fibs(6))//1, 5
    {
        Console.WriteLine(fib + " ");//4, 10
    }            
}

static IEnumerable<int> Fibs(int fibCount)
{
    for (int i = 0, prevFib = 0, currFib = 1; i < fibCount; i++)//2
    {
        yield return prevFib;//3, 9
        int newFib = prevFib + currFib;//6
        prevFib = currFib;//7
        currFib = newFib;//8
    }
}

此外,每个枚举都保留了它自己的状态。假设我再次调用 Fibs() 方法,则其状态将被重置。


3
设 prevFib = 1 - 第一个斐波那契数是“1”,而不是“0”。 - fubo

38

如果我理解得正确,这是我从实现IEnumerable与yield的函数的角度表述的方式:

  • 这里有一个。
  • 如果您需要另一个,请再次调用。
  • 我会记住我已经给过你什么。
  • 只有当您再次调用时,我才会知道是否可以再给您另一个。

4
简单而卓越 - Harry

35

直觉上,yield 关键字在不退出函数的情况下从函数返回值。例如,在您的代码示例中它返回当前的 item 值,然后继续执行循环。更正式地讲,编译器用它来生成一个迭代器的代码。迭代器是返回 IEnumerable 对象的函数。 MSDN 有关于迭代器的若干 文章


4
准确来说,它不会恢复循环,而是暂停循环,直到父级调用“iterator.next()”。 - Alex from Jitbit
9
这就是为什么我使用了“直观地”和“更正式地”的原因。 - Konrad Rudolph

34
这里有一个简单的方法来理解这个概念: 基本思想是,如果你想要一个可以使用"foreach"的集合,但由于某些原因(如从数据库中查询它们)收集项目到集合中很昂贵,并且你经常不需要整个集合,那么你可以创建一个函数,按项构建集合并将其逐项地“yield”回给消费者(然后消费者可以提前终止集合操作)。
这样想吧:你去肉柜台想买一磅切片火腿。屠夫拿了10磅的火腿到后面,放在切片机上,把整个东西切好,然后把切片堆带回来给你,再量出一磅给你。(老方法) 使用yield,屠夫把切片机带到柜台上,开始切片并将每个切片“yield”到秤上,直到重量达到1磅,然后为你包好就可以了。老方法可能对屠夫更好(让他按自己喜欢的方式组织机器),但新方法在大多数情况下对消费者来说显然更有效率。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接