What is the yield keyword used for in C#?
在"How can I expose only a fragment of ilist<>question"中,其中一个答案包含以下代码片段:
1 2 3 4 5 6 7 8 | IEnumerable<object> FilteredList() { foreach( object item in FullList ) { if( IsItemInPartialList( item ) yield return item; } } |
yield关键字在那里做什么?我在一些地方看到过它的引用,还有一个问题,但我还没有完全弄清楚它实际上是做什么的。我习惯于从一根线屈服于另一根线的角度来考虑屈服,但这在这里似乎并不相关。
yield关键字实际上在这里做了很多工作。函数返回实现IEnumerable接口的对象。如果调用函数开始对该对象进行调用,则再次调用该函数,直到它"生成"。这是C 2.0中引入的句法糖。在早期版本中,您必须创建自己的IEnumerable和IEnumerator对象来执行类似的操作。
理解这种代码的最简单方法是在一个示例中键入,设置一些断点,然后看看会发生什么。
尝试单步执行此操作,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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"行不再执行。
这是一个真实的例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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); } } } } } |
迭代。它创建了一个"在封面下"的状态机,它可以记住您在函数的每个附加循环中所处的位置,并从中获取信息。
产量有两大用途,
它有助于在不创建临时集合的情况下提供自定义迭代。
它有助于进行有状态的迭代。
为了更明确地解释以上两点,我制作了一个简单的视频,你可以在这里观看。
最近,RaymondChen也发表了一系列关于yield关键字的有趣文章。
- C语言中迭代器的实现及其后果(第1部分)
- C语言中迭代器的实现及其后果(第2部分)
- C语言中迭代器的实现及其后果(第3部分)
- C语言中迭代器的实现及其后果(第4部分)
虽然它名义上用于轻松实现迭代器模式,但可以概括为状态机。引用Raymond没有意义,最后一部分还链接到其他用途(但是Entin的博客中的例子特别好,展示了如何编写异步安全代码)。
第一眼,产量回报是一种纯糖,可以回报。
3.Without yield,all the items of the collection are created at once:
ZZU1
SAME code using yield,it returns item by item:
1 2 3 4 5 6 7 8 9 10 11 |
使用产量的优点是,如果功能消耗了你的数据,简单地需要收集的第一个项目,项目的其余部分就不会产生。
应要求,产量算子允许项目的创建。这是一个很好的理由。
与调查者一起使用。在每一次呼唤产量声明时,控制权都回到呼叫者手中,但这意味着Callee's State is maintained.由于这个原因,当Caller Enumerates the next element时,它继续在Callee方法中执行,从EDOCX1&1之后立即声明。
让我们以一个例子来理解这一点。在这个例子中,对应于每一条线路,我在执行流量中提到了顺序。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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 } } |
此外,国家对每一项调查都有控制权。假设,我有另一个电话给
直观地说,关键字从函数返回一个值而不离开它,即在代码示例中,它返回当前的
一个列表或阵列实现负荷的所有项目立即在产量实现时提供一个缺陷执行方案。
在实践中,这往往是一种令人沮丧的情况,因为需要在工作中达到最小的数量,以减少资源消耗的一种应用。
例如,我们可以在一个数据库中应用数以百万计的记录。The following benefits can be achieved when we use ienumerable in a deferred execution pull-based model:
- Scalability,reliability and predictibility are likely to improve,since the number of records does not impressicantly impact the application's resource requirements.
- 性能和响应性是自从处理能够立即开始等待整个收藏成为第一个装载。
- 由于应用可以停止、开始、中断或失败,因此恢复能力和使用是可以改进的。只有进步中的项目才能与实际使用结果的一部分的所有数据进行比较。
- 在添加恒定工作负荷流的环境中,可进行连续处理。
这是一个比较,比如说一张清单,用产量来比较。
List Example
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 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(); } |
控制台输出
联系人:创建联系人1
联系人2
联系人3
准备通过收藏重新开始。
注:整个收藏都装入了记忆中,甚至没有要求在列表中列入一个单一项目。
Yield Example
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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(); } |
控制台输出
准备通过收藏重新开始。
注:收藏并非一律执行。这是由于"有缺陷的执行"的性质。建设一个项目只会在需要时出现。
让我们重新召唤收藏,在我们第一次接触收藏的时候,抛弃行为。
1 2 3 4 5 6 7 8 | 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(); } |
控制台输出
准备通过收藏重新开始
联系人1
你好鲍勃
尼斯只有当客户"推出"项目时,才构建第一个接触点。
以下是理解概念的简单方法:基本思想是,如果您想要一个集合,您可以在上面使用"
这样想:你到肉柜台去买一磅火腿片。屠夫把一个10磅重的火腿放在后面,放在切片机上,把整条火腿切片,然后把一堆切片拿回来,量出一磅。(旧路)。有了
迭代器块可以被描述为语法糖,在这里编译器生成一个状态机,跟踪枚举的进展情况。要枚举可枚举的,通常使用
假设您有一个非常简单的迭代器块:
1 2 3 4 5 6 7 8 9 10 | 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"); } |
真正的迭代器块通常有条件和循环,但是当检查条件并展开循环时,它们最终仍然是与其他代码交错的
要枚举迭代器块,使用
1 2 | foreach (var i in IteratorBlock()) Console.WriteLine(i); |
这里是输出(这里没有惊喜):
1 2 3 4 5 6 7 | Begin 1 After 1 2 After 2 42 End |
如上所述,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | IEnumerator<int> enumerator = null; try { enumerator = IteratorBlock().GetEnumerator(); while (enumerator.MoveNext()) { var i = enumerator.Current; Console.WriteLine(i); } } finally { enumerator?.Dispose(); } |
为了解开这个谜团,我用板条箱装了一个序列图,去掉了抽象部分:
编译器生成的状态机也实现了枚举器,但为了使图表更清晰,我将它们作为单独的实例显示出来。(当从另一个线程枚举状态机时,您实际上会得到单独的实例,但这里的细节并不重要。)
每次调用迭代器块时,都会创建状态机的新实例。但是,在
1 | var evenNumbers = IteratorBlock().Where(i => i%2 == 0); |
此时迭代器尚未执行。
1 2 | foreach (var evenNumber in evenNumbers) Console.WriteLine(eventNumber); |
如果枚举可枚举的两次,则每次都会创建状态机的新实例,并且迭代器块将执行相同的代码两次。
注意,像
简单地说,c yield关键字允许对代码体(称为迭代器)进行多次调用,这些代码体知道如何在完成之前返回,当再次调用时,将在停止的位置继续执行,即,它有助于迭代器在迭代器在连续调用中返回的序列中对每个项透明地具有状态。
在JavaScript中,相同的概念称为生成器。
如果我正确理解这一点,下面将从实现IEnumerable和yield的函数的角度来描述这一点。
- 这里有一个。
- 如果您需要其他电话,请再次致电。
- 我会记得我给你的。
- 我只知道你再打电话时我能不能再给你一个。
这是为对象创建可枚举的非常简单和容易的方法。编译器创建一个类,该类包装您的方法,并在本例中实现IEnumerable
它产生了可枚举序列。它所做的是实际创建本地IEnumerable序列并将其作为方法结果返回
这个链接有一个简单的例子
这里有更简单的例子
1 2 3 4 | public static IEnumerable<int> testYieldb() { for(int i=0;i<3;i++) yield return 4; } |
注意,yield返回不会从方法返回。你甚至可以把一个
上面生成的IEnumerable为4 ints 4,4,4,4
这里有一个
1 2 3 4 5 6 | public static IEnumerable<int> testYieldb() { yield return 4; console.WriteLine("abc"); yield return 4; } |
还要注意,当使用yield时,返回的类型与函数的类型不同。它是
使用yield和方法的返回类型作为
为了让它执行,你必须以一种特殊的方式调用它。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | static void Main(string[] args) { testA(); Console.Write("try again. the above won't execute any of the function! "); foreach (var x in testA()) { } Console.ReadLine(); } // static List<int> testA() static IEnumerable<int> testA() { Console.WriteLine("asdfa"); yield return 1; Console.WriteLine("asdf"); } |
它试图带来一些红宝石的好处:)概念:这是一些打印出数组中每个元素的示例Ruby代码
1 2 3 4 | rubyArray = [1,2,3,4,5,6,7,8,9,10] rubyArray.each{|x| puts x # do whatever with x } |
数组的每个方法实现都将控制权交给调用者("puts x"),数组的每个元素整齐地呈现为x。然后调用者可以对x做任何它需要做的事情。
然而.NET并不是一路走到这里。C似乎将yield与ienumerable结合在一起,在某种程度上迫使您在调用者中编写foreach循环,如mendelt的响应所示。稍微不那么优雅。
1 2 3 4 5 6 7 8 9 10 11 12 13 | //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]; } |