在" 我如何仅公开IList的片段<>问题"中,其中一个答案包含以下代码段:
IEnumerable
yield关键字有什么作用?我已经看到它在几个地方被引用,另外一个问题,但我还没弄清楚它实际上做了什么.我习惯于在一个线程产生另一个线程的意义上考虑收益率,但这似乎并不重要.
yield关键字实际上在这里做了很多.该函数返回一个实现IEnumerable接口的对象.如果一个调用函数开始对该对象进行预处理,则再次调用该函数,直到它"产生"为止.这是C#2.0中引入的语法糖.在早期版本中,您必须创建自己的IEnumerable和IEnumerator对象来执行此类操作.
理解这样的代码的最简单方法是键入一个示例,设置一些断点并查看会发生什么.
尝试单步执行此操作,例如:
public void Consumer() { foreach(int i in Integers()) { Console.WriteLine(i.ToString()); } } public IEnumerableIntegers() { 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 IEnumerableRead (string sql, Func 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); } } } } }
迭代.它创建了一个"幕后"的状态机,可以记住你在函数的每个附加周期中的位置并从中获取.
产量有两大用途,
它有助于提供自定义迭代而无需创建临时集合.
它有助于进行有状态迭代.
为了更具说明性地解释上述两点,我创建了一个简单的视频,你可以在这里观看
最近,Raymond Chen还在yield关键字上发表了一系列有趣的文章.
C#中迭代器的实现及其后果(第1部分)
C#中迭代器的实现及其后果(第2部分)
C#中迭代器的实现及其后果(第3部分)
C#中迭代器的实现及其后果(第4部分)
虽然它名义上用于轻松实现迭代器模式,但可以推广到状态机.没有必要引用Raymond,最后一部分也链接到其他用途(但Entin的博客中的例子非常好,显示了如何编写异步安全代码).
乍一看,yield return是一个返回IEnumerable的.NET糖.
如果没有yield,则会立即创建集合的所有项:
class SomeData { public SomeData() { } static public IEnumerableCreateSomeDatas() { return new List { new SomeData(), new SomeData(), new SomeData() }; } }
使用yield的相同代码,它逐项返回:
class SomeData { public SomeData() { } static public IEnumerableCreateSomeDatas() { yield return new SomeData(); yield return new SomeData(); yield return new SomeData(); } }
使用yield的优点是,如果消耗数据的函数只需要集合的第一项,则不会创建其余项.
yield操作符允许根据需要创建项目.这是使用它的一个很好的理由.
yield return
与枚举器一起使用.在每次调用yield语句时,控制权返回给调用者,但它确保维持被调用者的状态.因此,当调用者枚举下一个元素时,它会在语句后面的yield
语句中继续执行callee方法.
让我们试着通过一个例子来理解这一点.在这个例子中,对应于每一行,我已经提到了执行流程的顺序.
static void Main(string[] args) { foreach (int fib in Fibs(6))//1, 5 { Console.WriteLine(fib + " ");//4, 10 } } static IEnumerableFibs(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()
方法调用,然后状态将被重置为它.
直观地说,关键字从函数返回一个值而不离开它,即在代码示例中它返回当前item
值然后恢复循环.更正式地说,编译器使用它来为迭代器生成代码.迭代器是返回IEnumerable
对象的函数.在MSDN有一些文章对他们.
列表或数组实现立即加载所有项,而yield实现提供延迟执行解决方案.
在实践中,通常希望根据需要执行最少量的工作以减少应用程序的资源消耗.
例如,我们可能有一个处理来自数据库的数百万条记录的应用程序.当我们在延迟执行基于拉的模型中使用IEnumerable时,可以实现以下好处:
可扩展性,可靠性和可预测性可能会提高,因为记录数量不会显着影响应用程序的资源需求.
性能和响应性可能会提高,因为处理可以立即开始,而不是等待首先加载整个集合.
由于应用程序可以停止,启动,中断或失败,因此可恢复性和利用率可能会提高.与预先获取仅使用实际使用的部分结果的所有数据相比,仅丢失正在进行的项目.
在添加常量工作负载流的环境中,可以进行连续处理.
下面是构建一个集合(如列表与使用yield)之间的比较.
列表示例
public class ContactListStore : IStore{ public IEnumerable GetEnumerator() { var contacts = new List (); 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{ public IEnumerable 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
太好了!当客户端将项目"拉出"集合时,仅构建了第一个联系人.
这是一个理解这个概念的简单方法:基本思想是,如果你想要一个可以使用" foreach
"的集合,但由于某种原因(比如从数据库中查询它们)将项目收集到集合中是昂贵的,并且您通常不需要整个集合,然后您创建一个函数,一次构建一个项目并将其返回给消费者(然后可以提前终止收集工作).
可以这样想:你去肉类柜台,想要买一磅切好的火腿.屠夫把一个10磅重的火腿放在后面,把它放在切片机上,切成整片,然后将一堆切片带回给你,并测出一磅.(旧方式).随着yield
,屠夫将切片机带到柜台,然后开始切片并"切割"每个切片到秤上,直到它测量到1磅,然后为你包裹它,你就完成了.对于屠夫而言,旧方式可能更好(让他按照自己喜欢的方式组织他的机器),但对于消费者而言,新方式在大多数情况下显然更有效.
该yield
关键字允许您IEnumerable
在迭代器块上的表单中创建.这个迭代器块支持延迟执行,如果你不熟悉这个概念,它可能看起来几乎是神奇的.但是,在一天结束时,它只是执行代码而没有任何奇怪的技巧.
迭代器块可以被描述为语法糖,其中编译器生成状态机,该状态机跟踪可枚举枚举的进度.要枚举可枚举,您经常使用foreach
循环.然而,foreach
循环也是语法糖.因此,您从实际代码中删除了两个抽象,这就是为什么它最初可能很难理解它是如何一起工作的.
假设您有一个非常简单的迭代器块:
IEnumerableIteratorBlock() { 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);
这是输出(这里没有惊喜):
Begin 1 After 1 2 After 2 42 End
如上所述foreach
是语法糖:
IEnumeratorenumerator = null; try { enumerator = IteratorBlock().GetEnumerator(); while (enumerator.MoveNext()) { var i = enumerator.Current; Console.WriteLine(i); } } finally { enumerator?.Dispose(); }
为了解开这个问题,我创建了一个删除了抽象的序列图:
编译器生成的状态机也实现了枚举器,但为了使图更清晰,我将它们显示为单独的实例.(当从另一个线程枚举状态机时,您实际上会获得单独的实例,但这里的详细信息并不重要.)
每次调用迭代器块时,都会创建一个新的状态机实例.但是,迭代器块中的所有代码都不会执行,直到enumerator.MoveNext()
第一次执行.这是延迟执行的工作原理.这是一个(相当愚蠢)的例子:
var evenNumbers = IteratorBlock().Where(i => i%2 == 0);
此时迭代器尚未执行.该Where
子句创建一个新的IEnumerable
包装IEnumerable
返回者,IteratorBlock
但是这个枚举还没有被枚举.执行foreach
循环时会发生这种情况:
foreach (var evenNumber in evenNumbers) Console.WriteLine(eventNumber);
如果枚举可枚举的两次,则每次都会创建一个新的状态机实例,并且迭代器块将执行两次相同的代码.
请注意,LINQ方法,如ToList()
,ToArray()
,First()
,Count()
等会使用一个foreach
循环来枚举枚举.例如,ToList()
将枚举可枚举的所有元素并将它们存储在列表中.您现在可以访问列表以获取可枚举的所有元素,而无需再次执行迭代器块.在使用CPU多次生成可枚举元素和使用类似方法存储枚举元素以多次访问它们之间需要进行权衡ToList()
.
如果我理解正确的话,这就是我如何从使用yield实现IEnumerable的函数的角度来说明这一点.
这是一个.
如果您需要另一个,请再次致电
我会记得我已经给你的东西.
我再次打电话时,我才会知道是否能给你另一个.
简单地说,C#yield关键字允许对一个代码体(称为迭代器)的多次调用,它知道如何在它完成之前返回,并且当再次调用时,继续它停止的地方 - 即它有助于迭代器对迭代器在连续调用中返回的序列中的每个项目变为透明状态.
在JavaScript中,相同的概念称为生成器.
这是为对象创建可枚举的一种非常简单易用的方法.编译器创建一个包装您的方法的类,并在这种情况下实现IEnumerable