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


827

在“ 如何仅显示IList <>的片段”问题中,答案之一具有以下代码片段:

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

yield关键字在那里做什么?我已经在几个地方提到过它,还有另一个问题,但是我还没有弄清楚它的实际作用。我习惯于从一个线程向另一个线程屈服的角度来考虑yield,但这在这里似乎无关紧要。


只是有关MSDN的链接,位于此处msdn.microsoft.com/en-us/library/vstudio/9k7k7cf0.aspx
开发人员

14
这不足为奇。困惑来自这样一个事实:我们有条件将“返回”视为函数输出,而在其之前没有“收益”。
拉里

4
我已经看过文档,但恐怕我还是听不懂:(
Ortund

Answers:


737

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

113
在这种情况下,这会更容易,我只是在这里使用整数来显示收益率的工作原理。关于使用yield return的好处是,这是实现迭代器模式的非常快速的方法,因此对事情的评估很懒。
Mendelt

110
另外,yield break;当您不想退回任何物品时,也可以使用。
罗里

7
yield不是关键字。如果是这样,我将无法使用yield作为标识符,例如int yield = 500;
Brandin

5
@Brandin是因为所有编程语言都支持两种类型的关键字,即保留关键字和上下文关键字。yield属于后面的类别,这就是为什么C#编译器不禁止您的代码的原因。此处有更多详细信息:ericlippert.com/2009/05/11/reserved-and-contextual-keywords您会很高兴地知道,还有一些保留词未被语言识别为关键字。例如java中的goto。此处有更多详细信息:stackoverflow.com/questions/2545103/…–
RBT

6
'If a calling function starts foreach-ing over this object the function is called again until it "yields"'。对我来说听起来不对。我总是在“农作物丰收”而不是“汽车给行人增产”的背景下想到c#yield关键字。
扎克

369

迭代。它在“隐藏”状态下创建一个状态机,该状态机会记住您在该函数的每个其他循环中所处的位置,然后从那里开始。


209

收益有两个重大用途,

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

  2. 它有助于进行有状态的迭代。 在此处输入图片说明

为了更说明性地解释上述两点,我创建了一个简单的视频,您可以在这里观看


13
该视频帮助我清楚地理解了yield。@ShivprasadKoirala的代码项目文章C#Yield的用途是什么?相同的解释也是一个很好的来源
Dush

我还要补充一点,这yield是创建自定义IEnumerator的“快速”方法(而不是让类实现IEnumerator接口)。
MrTourkos

我观看了您的视频Shivprasad,它清楚地说明了yield关键字的用法。
Tore Aurstad

感谢您的视频!很好解释!
Roblogic

很棒的视频,但是很奇怪...使用yield的实现显然更干净,但是它必须本质上是在内部创建自己的临时内存或/和List,以便跟踪状态(或更确切地说是创建状态机)。那么,“ Yield”除了使实现更简单,使外观看起来更好还是在做其他事情吗?效率如何,使用Yield运行代码会比没有效率更高/更低?
难题

135

最近,Raymond Chen也对yield关键字进行了一系列有趣的文章。

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


这需要进行表决。甜蜜的他如何解释操作员和内部的目的。
sajidnizami 2011年

3
第1部分解释了“收益回报”的句法糖。很好的解释!
Dror Weiss

99

乍一看,收益回报率是返回IEnumerable.NET糖。

如果没有收益,则立即创建集合的所有项目:

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


40

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


2
设置prevFib = 1-第一个斐波那契数字是“ 1”,而不是“ 0”
Fubo 2015年

31

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


4
好吧,确切地说,它不会恢复循环,而是将其暂停,直到父级调用“ iterator.next()”。
亚历克斯

8
@jitbit这就是为什么我“直观地”和“更正式地”使用的原因。
Konrad Rudolph

31

列表或数组实现立即加载所有项目,而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();
}

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

注意:集合根本没有执行。这是由于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

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


1
这个答案需要更多关注!Thx
leon22

@ leon22绝对+2
snr

26

这是一种理解概念的简单方法:基本思想是,如果您希望可以在其上使用“ foreach”的集合,但是由于某些原因(例如从数据库中查询它们)将项目收集到集合中会很昂贵,而且,您通常不需要整个集合,然后创建一个函数,一次生成一个集合并将其交还给消费者(后者可以尽早终止收集工作)。

这样想:您去肉店买一磅火腿片。屠夫将10磅重的火腿放回去,放在切片机上,切成薄片,然后将一堆薄片带回去,并从中取出一磅。(旧方法)。用yield,屠夫将切片机带到柜台,然后开始切片并将每个切片“屈服”到秤上,直到测量到1磅重,然后将其包裹起来就完成了。对于屠夫而言,“旧方式”可能更好(让他按照自己喜欢的方式组织机器),但在大多数情况下,“新方式”对于消费者来说效率更高。


18

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

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

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


18

如果我正确理解这一点,那么从实现带有yield的IEnumerable的函数的角度来看,这就是我的措辞。

  • 这是一个
  • 如果您需要其他电话,请再次致电。
  • 我会记得我已经给你的。
  • 当您再次打电话时,我只能告诉您是否能再给您另一个。

简单而辉煌
哈里

10

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

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


最好的解释呢。这些也是python中相同的生成器吗?
petrosmm

7

这是为对象创建枚举的非常简单的方法。编译器将创建一个包装您的方法的类,并实现IEnumerable <object>。如果没有yield关键字,则必须创建一个实现IEnumerable <object>的对象。


5

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


3

链接有一个简单的例子

更简单的例子在这里

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

注意,收益率回报不会从方法中回报。您甚至可以WriteLineyield return

上面产生的IEnumerable为4 int 4,4,4,4

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

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

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

您可以将yield与方法的返回类型一起使用IEnumerable。如果该方法的返回类型为intList<int>并且您使用yield,则它将不会编译。您可以使用IEnumerable不带yield的方法返回类型,但似乎您不能不带yield来使用IEnumerable方法返回类型。

为了使其执行,您必须以特殊方式调用它。

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

注意-如果想了解的SelectMany,它采用的产量也仿制药..这个例子可以帮助public static IEnumerable<TResult> testYieldc<TResult>(TResult t) { yield return t; }public static IEnumerable<TResult> testYieldc<TResult>(TResult t) { return new List<TResult>(); }
barlop

看起来很好解释!这可能是公认的答案。
pongapundit

@pongapundit谢谢,我的回答肯定很简单,但是我自己并没有使用过yield,其他答复者比我更了解它,并且对它的用法了解更多。我在这里写的yield可能是因为我挠头试图找出此处和该dotnetperls链接的一些答案!但是,由于我不太了解yield return(除了我提到的简单的事情),并且没有太多使用它,也不太了解它的用途,因此我认为这不应该被接受。
barlop

3

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


-3

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

 rubyArray = [1,2,3,4,5,6,7,8,9,10]
    rubyArray.each{|x| 
        puts x   # do whatever with 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]; 
}

7
-1这个答案对我来说听起来不正确。是的,C#yield与耦合IEnumerable,并且C#缺少Ruby的“块”概念。但是C#具有lambda,可以ForEach像Ruby一样允许方法的实现each但是,这并不意味着这样做是一个好主意
rsenna

更好的是:public IEnumerable <int> Each(){int index = 0; 收益回报数据[index ++]; }
ata 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.