在C#中使用的yield关键字是什么?
在我怎样才能暴露只有一个IList <>问题的一个问题之一的答案有以下代码片段:
IEnumerable<object> FilteredList() { foreach( object item in FullList ) { if( IsItemInPartialList( item ) yield return item; } }
yield关键字在那里做什么? 我已经在几个地方看到了这个引用,还有一个问题,但是我还没有弄明白它到底在做什么。 我习惯于从一个线程产生另一个线程的意义上考虑产量,但在这里看起来并不重要。
yield关键字实际上在这里做了很多。 该函数返回一个实现IEnumerable接口的对象。 如果一个调用函数开始对这个对象进行foreach,那么函数会被再次调用,直到它“屈服”。 这是C#2.0中引入的语法糖。 在早期的版本中,你必须创build自己的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); } } } } }
迭代。 它创build了一个“下”的状态机,它记住了每个附加循环的function,并从那里开始。
最近Raymond Chen也在yield关键字上发表了一系列有趣的文章。
- C#中迭代器的实现及其后果(第一部分)
- C#中迭代器的实现及其后果(第2部分)
- C#中迭代器的实现及其后果(第3部分)
- C#中迭代器的实现及其后果(第4部分)
虽然它名义上用于轻松实现迭代器模式,但可以概括为状态机。 在引用雷蒙德的时候,最后一部分也没有引用其他用途(但Entin博客中的例子特别好,展示了如何编写asynchronous安全代码)。
收益率有两个很大的用途,
-
它有助于提供自定义迭代而不创build临时集合。
-
它有助于做有状态的迭代。
为了更加明确地解释以上两点,我创build了一个简单的video,你可以在这里观看
乍看之下,yield yield是一个返回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的好处在于,如果使用数据的函数只需要收集的第一个项目,其余的项目将不会被创build。
收益运算符允许按需要创build项目。 这是使用它的一个很好的理由。
直观地说,关键字从函数中返回一个值,而不离开它,也就是说,在你的代码示例中,它返回当前item
值,然后恢复循环。 更正式地,编译器使用它来为迭代器生成代码。 迭代器是返回IEnumerable
对象的函数。 MSDN有几篇关于他们的文章 。
yield return
与统计员一起使用。 在yield语句的每个调用中,控制都返回给调用者,但是它确保了被调用者的状态得以维持。 因此,当调用者枚举下一个元素时,它将在yield
语句之后立即继续在callee方法中执行。
让我们试着用一个例子来理解这一点。 在这个例子中,对应于每一行我已经提到了执行stream程的顺序。
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()
方法,那么状态将被重置。
列表或数组实现立即加载所有项目,而yield实现提供延迟执行解决scheme。
实际上,为了减less应用程序的资源消耗,通常需要根据需要执行最小量的工作。
例如,我们可能有一个应用程序处理来自数据库的数百万条logging。 当我们在延迟执行的基于拉的模型中使用IEnumerable时,可以获得以下好处:
- 可扩展性,可靠性和可预测性可能会提高,因为logging数量不会显着影响应用程序的资源需求。
- 性能和响应能力可能会提高,因为处理可以立即开始,而不是等待整个集合被首先加载。
- 由于应用程序可以停止,启动,中断或失败, 可恢复性和利用率可能会提高。 与预取全部数据相比,只有正在使用的部分结果才会丢失。
- 在持续工作负载stream被添加的环境中可以进行连续处理 。
这里比较一下使用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:创build联系人1
ContactListStore:创build联系人2
ContactListStore:创build联系人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的“延迟执行”性质。 构build一个项目只会发生在真正需要的时候。
让我们再次调用集合,并且当我们获取集合中的第一个联系人时,会正确地处理这个行为。
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:创build联系人1
鲍勃你好
太好了! 当客户从collections中“拉出”物品时,只有第一个联系人被build造。
这里有一个简单的方法来理解这个概念:基本的想法是,如果你想要一个可以使用“ foreach
”的集合,但是收集项目到集合中是昂贵的,因为某些原因(比如从数据库中查询它们) ,而且你通常不需要整个集合,那么你创build一个函数,一次构build一个集合,然后把它交给消费者(然后可以尽早终止这个集合)。
想想这样:你去肉柜台,想买一磅火腿片。 屠夫把一个10磅重的火腿放在后面,放在切片机上,切成片,然后把一堆切片放回给你,并测出一磅。 (旧方式)。 随着yield
,屠夫把切片机送到柜台,并开始切片,“屈服”每个切片到秤上,直到它测量到1磅,然后包装给你,你就完成了。 对于屠夫来说,旧方式可能会更好(让他按自己喜欢的方式组织自己的机器),但对于消费者来说,新方法在大多数情况下显然更有效率。
简单地说,C#yield关键字允许对称为迭代器的代码体进行许多调用,它知道如何在完成之前返回,并在再次调用时继续它停止的地方 – 即帮助迭代器在迭代器在连续调用中返回的序列中,每个项目都变为透明状态。
在JavaScript中,同样的概念被称为Generators。
这是一个非常简单和容易的方法来为您的对象创build一个枚举。 编译器创build一个包装你的方法的类,在这种情况下,它实现IEnumerable <object>。 如果没有yield关键字,则必须创build一个实现IEnumerable <object>的对象。
yield
关键字允许您在迭代器块的表单中创build一个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(); }
为了解决这个问题,我编写了一个序列图,删除了抽象:
由编译器生成的状态机也实现了枚举器,但为了使图更清晰,我将它们作为单独的实例展示出来。 (当从另一个线程枚举状态机时,您确实获得了单独的实例,但是这里的细节并不重要)。
每次调用迭代器块时,都会创build一个新的状态机实例。 但是,只有在enumerator.MoveNext()
器enumerator.MoveNext()
第一次执行之前,您的代码才会被执行。 这是推迟执行的工作方式。 这是一个(相当愚蠢的)例子:
var evenNumbers = IteratorBlock().Where(i => i%2 == 0);
此时迭代器没有执行。 Where
子句创build一个新的IEnumerable<T>
,它封装了由IteratorBlock
返回的IEnumerable<T>
,但是这个枚举还没有被枚举。 这发生在你执行一个foreach
循环时:
foreach (var evenNumber in evenNumbers) Console.WriteLine(eventNumber);
如果枚举两次,那么每次创build一个新的状态机实例,迭代器块将执行两次相同的代码。
注意像ToList()
, ToArray()
, First()
, Count()
等LINQ方法将使用foreach
循环来枚举枚举。 例如ToList()
将枚举枚举的所有元素并将它们存储在一个列表中。 您现在可以访问该列表以获取枚举的所有元素,而不会再次执行迭代器块。 在使用CPU来产生多次可枚举元素和存储器之间存在权衡,以便在使用像ToList()
这样的方法时存储枚举元素多次访问它们。
它产生了可枚举的序列。 它所做的实际上是创build本地IEnumerable序列并将其作为方法结果返回
这个链接有一个简单的例子
更简单的例子在这里
public static IEnumerable<int> testYieldb() { for(int i=0;i<3;i++) yield return 4; }
注意yield yield不会从方法中返回。 您甚至可以在yield return
之后放入一个WriteLine
以上产生一个IEnumerable4,4,4,4,4
这里有一个WriteLine
。 将列表添加4,打印abc,然后将4添加到列表中,然后完成方法,然后真正从方法返回(一旦方法已经完成,就像没有返回的过程一样)。 但是,这将有一个值, IEnumerable
int
s列表,它完成时返回。
public static IEnumerable<int> testYieldb() { yield return 4; console.WriteLine("abc"); yield return 4; }
还要注意,当你使用yield时,你所返回的与函数的types不一样。 它是IEnumerable
列表中元素的types。
您将方法的返回types使用yield作为IEnumerable
。 如果方法的返回types是int
或List<int>
并且使用了yield
,那么它将不会被编译。 您可以使用IEnumerable
方法返回types没有收益,但似乎也许你不能使用没有IEnumerable
方法返回types的收益率。
为了执行它,你必须以特殊的方式来调用它。
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"); }
这是试图引入一些Ruby善良:)
概念:这是一些打印出数组的每个元素的示例Ruby代码
rubyArray = [1,2,3,4,5,6,7,8,9,10] rubyArray.each{|x| puts x # do whatever with x }
数组的每个方法实现都将控制权交给调用者('puts x'),数组中的每个元素整齐地表示为x。 调用者然后可以做任何需要做的事情。
然而.Net并不是一路走来的。C#似乎把IEnumerable和yield结合起来,这样就迫使你在调用者中编写一个foreach循环,就像在Mendelt的回应中看到的一样。 less一点优雅。
//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]; }