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

时间:2008-09-02 13:15:25

标签: c# yield

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

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

yield关键字有什么作用?我已经看到它在几个地方被引用,另外一个问题,但我还没弄清楚它实际上做了什么。我习惯于在一个线程产生另一个线程的意义上考虑收益率,但这似乎并不重要。

16 个答案:

答案 0 :(得分:654)

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

该函数返回一个实现IEnumerable<object>接口的对象。如果调用函数在此对象上启动foreach,则再次调用该函数,直到它“生成”。这是 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);
                }
            }
        }
    }
}

答案 1 :(得分:347)

迭代。它创建了一个“幕后”的状态机,可以记住你在函数的每个附加周期中的位置并从中获取。

答案 2 :(得分:184)

Yield有两个很好的用途,

  1. 有助于提供自定义迭代而无需创建临时集合。

  2. 有助于进行有状态迭代。 enter image description here

  3. 为了更具说明性地解释上述两点,我创建了一个简单的视频,你可以观看here

答案 3 :(得分:128)

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

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

答案 4 :(得分:34)

yield return与枚举器一起使用。在每次调用yield语句时,控制权返回给调用者,但它确保维持被调用者的状态。因此,当调用者枚举下一个元素时,它会在yield语句之后的语句中继续执行callee方法。

让我们试着通过一个例子来理解这一点。在这个例子中,对应于每一行,我提到了执行流程的顺序。

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()方法,那么将重置状态。

答案 5 :(得分:30)

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

答案 6 :(得分:24)

列表或数组实现立即加载所有项目,而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
准备好迭代整个系列。

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

收益率示例

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

控制台输出
准备好迭代整个系列。

注意:该集合根本没有执行。这是由于&#34;延迟执行&#34; 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
你好鲍勃

尼斯!当客户&#34;拉出&#34;时,只构建了第一个联系人。该系列中的物品。

答案 7 :(得分:22)

这是理解这个概念的简单方法: 基本的想法是,如果你想要一个可以使用的集合&#34; Microsoft.Csharp.Core.targets&#34; on,但是由于某些原因(例如从数据库中查询它们)将项目收集到集合中是昂贵的,并且您通常不需要整个集合,然后您创建一个函数,一次构建一个项目并产生它回到消费者(然后可以提前终止收集工作)。

这样想:你去肉类柜台,想要买一磅切好的火腿。屠夫将10磅重的火腿放在后面,将它放在切片机上,切成整片,然后将一堆切片带回给你并测出一磅。 (旧方式)。 使用foreach,屠夫将切片机带到柜台,然后开始切片并且&#34;屈服&#34;将每个切片放在刻度上,直到它达到1磅,然后将它包裹起来,然后你就完成了。 The Old Way对于屠夫来说可能更好(让他按自己喜欢的方式组织他的机器),但在大多数情况下,New Way显然对消费者更有效。

答案 8 :(得分:13)

yield关键字允许您在iterator block上的表单中创建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");
}

Real迭代器块通常具有条件和循环,但是当您检查条件并展开循环时,它们最终仍然以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();
}

为了解开这个问题,我创建了一个删除了抽象的序列图:

C# iterator block sequence diagram

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

每次调用迭代器块时,都会创建一个新的状态机实例。但是,在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()Count()等LINQ方法将使用foreach循环来枚举可枚举项。例如,ToList()将枚举可枚举的所有元素并将它们存储在列表中。您现在可以访问列表以获取可枚举的所有元素,而无需再次执行迭代器块。在使用CPU多次生成可枚举元素和使用枚举元素来存储枚举元素以便在使用ToList()等方法时多次访问它们之间存在权衡。

答案 9 :(得分:12)

如果我理解正确的话,这就是我从具有yield的IEnumerable实现的函数的角度来说明这一点。

  • 这是一个。
  • 如果您需要其他人,请再次致电。
  • 我会记得我已经给你的东西。
  • 我只会在你再打电话的时候知道我能再给你一个。

答案 10 :(得分:10)

简单地说,C#yield关键字允许对代码体(称为迭代器)进行多次调用,该代码体知道如何在它完成之前返回,并且当再次调用时,它继续在它离开的地方继续off - 即它有助于迭代器对迭代器在连续调用中返回的序列中的每个项目变为透明状态。

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

答案 11 :(得分:6)

这是为对象创建可枚举的一种非常简单的方法。编译器创建一个包装您的方法的类,并在这种情况下实现IEnumerable&lt; object&gt;。如果没有yield关键字,则必须创建一个实现IEnumerable&lt; object&gt;的对象。

答案 12 :(得分:4)

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

答案 13 :(得分:2)

这个link有一个简单的例子

更简单的例子就在这里

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

请注意,收益率回报不会从该方法返回。您甚至可以在WriteLine

之后加yield return

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

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

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

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

您使用yield的方法返回类型为IEnumerable。如果方法的返回类型为intList<int>并且您使用yield,那么它将无法编译。您可以使用IEnumerable方法返回类型而不会产生但似乎您可以在没有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");
}

答案 14 :(得分:0)

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

示例:使用收益率,即懒惰执行。

    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仅引用方法而不是项目。 这意味着它尚未执行在方法内部获取项目的过程。仅在需要时获取数据的一种非常有效的方法。 产量的实际实现可以在ORM中看到,例如Entity Framework和NHibernate等。

答案 15 :(得分:-2)

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

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

Array的每个方法实现产生控制到调用者('puts x'),其中数组的每个元素整齐地表示为x。然后,调用者可以执行x所需的任何操作。

然而 .Net 并不是一直都在这里.. C#似乎与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]; 
}