953

How Can I Expose Only a Fragment of IList<>问题中,其中一个答案具有以下代码片段:

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

yield 关键字在那里做什么?我在几个地方和另一个问题上看到过它的引用,但我还没有完全弄清楚它的实际作用。我习惯于从一个线程屈服于另一个线程的意义上考虑屈服,但这在这里似乎无关紧要。

4

19 回答 19

864

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

该函数返回一个实现IEnumerable<object>接口的对象。如果调用函数开始foreach对该对象进行调用,则再次调用该函数,直到它“屈服”。这是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);
                }
            }
        }
    }
}
于 2008-09-02T13:23:16.200 回答
408

迭代。它在“幕后”创建了一个状态机,该状态机记住您在函数的每个附加循环中所处的位置并从那里获取。

于 2008-09-02T13:17:23.250 回答
259

产量有两大用途,

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

  2. 它有助于进行有状态的迭代。 在此处输入图像描述

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

于 2013-04-12T17:31:15.980 回答
149

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

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

于 2008-09-02T13:27:46.767 回答
128

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

于 2015-01-17T14:23:08.620 回答
53

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

在实践中,通常希望根据需要执行最少量的工作,以减少应用程序的资源消耗。

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

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

这是先构建集合(例如列表)与使用产量的比较。

列表示例

    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
准备遍历集合。

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

产量示例

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: Creating contact 1
Hello Bob

好的!当客户将项目从集合中“拉出”时,仅构建了第一个联系人。

于 2015-12-04T19:52:36.617 回答
45

yield return与枚举器一起使用。在每次调用 yield 语句时,控制权都返回给调用者,但它确保保持被调用者的状态。因此,当调用者枚举下一个元素时,它会在语句之后立即继续在被调用者方法 from 语句中执行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()那么状态将被重置。

于 2014-02-25T17:15:45.473 回答
33

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

于 2008-09-02T13:19:09.533 回答
31

如果我理解正确,下面是我从实现 IEnumerable 和 yield 的函数的角度来表达这一点的方式。

  • 这是一个。
  • 如果您需要另一个,请再次致电。
  • 我会记住我已经给你的。
  • 我只知道当你再次打电话时我能不能再给你一个。
于 2018-04-15T22:28:47.840 回答
30

这是理解这个概念的一种简单方法:基本思想是,如果你想要一个可以使用“ foreach”的集合,但是由于某种原因将项目收集到集合中是昂贵的(比如从数据库中查询它们),而且您通常不需要整个集合,然后您创建一个函数,该函数一次构建一个项目并将其返回给消费者(然后消费者可以提前终止收集工作)。

可以这样想:你去肉类柜台想买一磅火腿片。屠夫将一根 10 磅重的火腿放在背后,放在切片机上,将整块火腿切片,然后将一堆切片拿回给你,量出一磅。(旧方式)。有了yield,屠夫将切片机带到柜台,开始切片并将每一片“产出”到秤上,直到它达到 1 磅,然后为你包装好,你就完成了。旧方式可能对屠夫更好(让他按照自己喜欢的方式组织机器),但在大多数情况下,新方式显然对消费者更有效。

于 2016-09-01T13:43:26.947 回答
27

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

这是输出(这里没有惊喜):

开始
1
1 之后
2
2 之后
42
结尾

如上所述foreach是语法糖:

IEnumerator<int> enumerator = null;
try
{
    enumerator = IteratorBlock().GetEnumerator();
    while (enumerator.MoveNext())
    {
        var i = enumerator.Current;
        Console.WriteLine(i);
    }
}
finally
{
    enumerator?.Dispose();
}

为了解决这个问题,我制作了一个删除抽象的序列图:

C#迭代器块序列图

编译器生成的状态机也实现了枚举器,但为了使图表更清晰,我将它们显示为单独的实例。(当从另一个线程枚举状态机时,您实际上会得到单独的实例,但这个细节在这里并不重要。)

每次调用迭代器块时,都会创建一个新的状态机实例。enumerator.MoveNext()但是,在第一次执行之前,不会执行迭代器块中的任何代码。这就是延迟执行的工作原理。这是一个(相当愚蠢的)示例:

var evenNumbers = IteratorBlock().Where(i => i%2 == 0);

此时迭代器还没有执行。该Where子句创建了一个新IEnumerable<T>的包装IEnumerable<T>返回的,IteratorBlock但此枚举尚未枚举。执行foreach循环时会发生这种情况:

foreach (var evenNumber in evenNumbers)
    Console.WriteLine(eventNumber);

如果您枚举可枚举两次,则每次都会创建一个新的状态机实例,并且您的迭代器块将执行相同的代码两次。

请注意,像ToList()ToArray()First()等LINQ 方法Count()将使用foreach循环来枚举可枚举对象。例如ToList()将枚举可枚举的所有元素并将它们存储在列表中。您现在可以访问列表以获取可枚举的所有元素,而无需再次执行迭代器块。在使用 CPU 多次生成可枚举元素和使用内存来存储枚举元素以在使用ToList().

于 2017-06-28T11:54:13.547 回答
18

Yield 关键字的一大亮点是延迟执行。现在我所说的延迟执行是在需要时执行。更好的表达方式是举个例子

示例:不使用 Yield 即没有延迟执行。

public static IEnumerable<int> CreateCollectionWithList()
{
    var list =  new List<int>();
    list.Add(10);
    list.Add(0);
    list.Add(1);
    list.Add(2);
    list.Add(20);

    return list;
}

示例:使用 Yield 即延迟执行。

public static IEnumerable<int> CreateCollectionWithYield()
{
    yield return 10;
    for (int i = 0; i < 3; i++) 
    {
        yield return i;
    }

    yield return 20;
}

现在当我调用这两种方法时。

var listItems = CreateCollectionWithList();
var yieldedItems = CreateCollectionWithYield();

您会注意到 listItems 里面有 5 个项目(在调试时将鼠标悬停在 listItems 上)。而 yieldItems 只会引用方法而不是项目。这意味着它没有执行在方法中获取项目的过程。仅在需要时获取数据的一种非常有效的方式。yield 的实际实现可以在 ORM 中看到,例如 Entity Framework 和 NHibernate 等。

于 2019-10-17T23:35:12.753 回答
13

简而言之,C# yield 关键字允许对称为迭代器的代码体进行多次调用,该代码体知道如何在完成之前返回,并且在再次调用时从中断处继续 - 即它有助于迭代器在迭代器在连续调用中返回的序列中,每个项目都变得透明有状态。

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

于 2013-01-14T22:49:09.723 回答
10

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

于 2008-09-02T13:17:36.340 回答
7

它正在产生可枚举的序列。它实际上是创建本地 IEnumerable 序列并将其作为方法结果返回

于 2008-09-02T13:18:25.493 回答
4

这个链接有一个简单的例子

更简单的例子在这里

public static IEnumerable<int> testYieldb()
{
    for(int i=0;i<3;i++) yield return 4;
}

请注意,yield return 不会从该方法返回。你甚至可以WriteLineyield return

以上产生了 4 个整数 4,4,4,4 的 IEnumerable

这里有一个WriteLine. 将 4 添加到列表中,打印 abc,然后将 4 添加到列表中,然后完成方法,然后真正从方法返回(一旦方法完成,就像没有返回的过程一样)。但这会有一个值,一个sIEnumerable列表int,它在完成时返回。

public static IEnumerable<int> testYieldb()
{
    yield return 4;
    console.WriteLine("abc");
    yield return 4;
}

另请注意,当您使用 yield 时,您返回的内容与函数的类型不同。IEnumerable它是列表中元素的类型。

您将 yield 与方法的返回类型一起使用为IEnumerable. 如果方法的返回类型是intorList<int>并且您使用yield,那么它将无法编译。您可以使用IEnumerable没有 yield 的方法返回类型,但似乎您不能使用没有IEnumerable方法返回类型的 yield。

为了让它执行,你必须以一种特殊的方式调用它。

static void Main(string[] args)
{
    testA();
    Console.Write("try again. the above won't execute any of the function!\n");

    foreach (var x in testA()) { }


    Console.ReadLine();
}



// static List<int> testA()
static IEnumerable<int> testA()
{
    Console.WriteLine("asdfa");
    yield return 1;
    Console.WriteLine("asdf");
}
于 2016-05-01T01:25:08.987 回答
1

现在,您可以将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;
        }
    }
}
于 2021-11-06T18:23:24.600 回答
-2

了解产量的简单演示

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;
                }
            }
        }
    } 
}
于 2021-10-01T17:51:20.643 回答
-5

它试图引入一些 Ruby Goodness :)
概念:这是一些示例 Ruby 代码,它打印出数组的每个元素

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

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

然而, .Net并没有一路走下去。C# 似乎将 yield 与 IEnumerable 结合在一起,以某种方式迫使您在调用者中编写一个 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]; 
}
于 2008-09-02T14:06:52.493 回答