从0遍历到20(不包括20),输出遍历到的每个元素,并将大于2的所有数字放到一个IEnumerable
解答1:(我以前经常这样做)
static IEnumerableWithNoYield() { IList list = new List (); for (int i = 0; i < 20; i++) { Console.WriteLine(i.ToString()); if(i > 2) list.Add(i); } return list; }
解答2:(自从有了C# 2.0我们还可以这样做)
static IEnumerableWithYield() { for (int i = 0; i < 20; i++) { Console.WriteLine(i.ToString()); if(i > 2) yield return i; } }
如果我用下面这样的代码测试,会得到怎样的输出?
测试1:
测试WithNoYield()
static void Main()
{
WithNoYield();
Console.ReadLine();
}
测试WithYield()
static void Main()
{
WithYield();
Console.ReadLine();
}
测试2:
测试WithNoYield()
static void Main()
{
foreach (int i in WithNoYield())
{
Console.WriteLine(i.ToString());
}
Console.ReadLine();
}
测试WithYield()
static void Main()
{
foreach (int i in WithYield())
{
Console.WriteLine(i.ToString());
}
Console.ReadLine();
}
给你5分钟时间给出答案,不要上机运行
*********************************5分钟后***************************************
测试1的运算结果
测试WithNoYield():输出从0-19的数字
测试WithYield():什么都不输出
测试2的运算结果
测试WithNoYield():输出1-19接着输出3-19
测试WithYield():输出12334455…….
(为节省空间上面的答案没有原样粘贴,可以自己运行测试)
是不是感到很奇怪,为什么使用了yield的程序表现的如此怪异呢?
测试1中对WithYield()的测试,明明方法调用了,居然一行输出都没有,难道for循环根本没有执行?通过断点调试果然如此,for循环根本没有进去,这是咋回事?测试2中对WithYield()的测试输出是有了,不过输出怎么这么有趣?穿插着输出,在foreach遍历WithYield()的结果的时候,好像不等到最后一条遍历完,WithYield()不退出,这又是怎么回事?
还是打开IL代码瞧一瞧到底发生了什么吧
Main方法的IL代码:
.method private hidebysig static void Main() cil managed { .entrypoint .maxstack 1 .locals init ( [0] int32 i, [1] class [mscorlib]System.Collections.Generic.IEnumerator`1CS$5$0000) L_0000: call class [mscorlib]System.Collections.Generic.IEnumerable`1 TestLambda.Program::WithYield() L_0005: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1 [mscorlib]System.Collections.Generic.IEnumerable`1 ::GetEnumerator() L_000a: stloc.1 L_000b: br.s L_0020 L_000d: ldloc.1 L_000e: callvirt instance !0 [mscorlib]System.Collections.Generic.IEnumerator`1 ::get_Current() L_0013: stloc.0 L_0014: ldloca.s i L_0016: call instance string [mscorlib]System.Int32::ToString() L_001b: call void [mscorlib]System.Console::WriteLine(string) L_0020: ldloc.1 L_0021: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext() L_0026: brtrue.s L_000d L_0028: leave.s L_0034 L_002a: ldloc.1 L_002b: brfalse.s L_0033 L_002d: ldloc.1 L_002e: callvirt instance void [mscorlib]System.IDisposable::Dispose() L_0033: endfinally L_0034: call string [mscorlib]System.Console::ReadLine() L_0039: pop L_003a: ret .try L_000b to L_002a finally handler L_002a to L_0034 }
这里没什么稀奇的,在上一篇我已经分析过了,foreach内部就是转换成调用迭代器的MoveNext()方法进行while循环。我浏览到WithYield()方法:
private static IEnumerable
{
return new
}
晕,怎么搞的,这是我写的代码么?我的for循环呢?经过我再三确认,确实是我写的代码生成的。我心里暗暗叫骂,编译器,你怎么能这样“无耻”,在背后修改我的代码,你这不侵权么。还给我新生成了一个类
现在能解答测试1为什么没有输出了,调用WithYield()里面就是调用了一下
public
{
this.<>1__state = <>1__state;
this.<>l__initialThreadId = Thread.CurrentThread.ManagedThreadId;
}
这里没有任何输出。
在测试2中,首先我们会调用
private bool MoveNext() { switch (this.<>1__state) { case 0: this.<>1__state = -1; this.5__1 = 0; goto Label_006A; case 1: this.<>1__state = -1; goto Label_005C; default: goto Label_0074; } Label_005C: this.5__1++; Label_006A: if (this.5__1 < 20) { Console.WriteLine(this.5__1.ToString()); if (this.5__1 > 2) { this.<>2__current = this.5__1; this.<>1__state = 1; return true; } goto Label_005C; } Label_0074: return false; }
原来我们for循环里面的Console.WriteLine跑到这里来了,所以没等到MoveNext()调用,for里面的输出也是不会被执行的,因为每次遍历都要访问MoveNext()方法,所以没有等到返回结果里面的元素遍历完WithYield()也是不会退出的。现在我们的测试程序所表现出来的怪异行为是可以找到依据了,那就是:编译器在后台搞了鬼。
实际上这种实现在理论上是有支撑的:延迟计算(Lazy evaluation或delayed evaluation)在Wiki上可以找到它的解释:将计算延迟,直到需要这个计算的结果的时候才计算,这样就可以因为避免一些不必要的计算而改进性能,在合成一些表达式时候还可以避免一些不必要的条件,因为这个时候其他计算都已经完成了,所有的条件都已经明确了,有的根本不可达的条件可以不用管了。反正就是好处很多了。
延迟计算来源自函数式编程,在函数式编程里,将函数作为参数来传递,你想呀,如果这个函数一传递就被计算了,那还搞什么搞,如果你使用了延迟计算,表达式在没有使用的时候是不会被计算的,比如有这样一个应用:x=expression,将这个表达式赋给x变量,但是如果x没有在别的地方使用的话这个表达式是不会被计算的,在这之前x里装的是这个表达式。
看来这个延迟计算真是个好东西,别担心,整个Linq就是建立在这之上的,这个延迟计算可是帮了Linq的大忙啊(难道在2.0的时候,微软就为它的Linq开始蓄谋了?),看下面的代码:
var result = from book in books where book.Title.StartWiths(“t”) select book if(state > 0) { foreach(var item in result) { //…. } }
result是一个实现了IEnumerable
如果上面这段代码,最后由于state小于0,而对result没有任何需求了,在Linq里返回的结果都是IEnumerable
延迟计算还有很多惟妙惟肖的特质,也许以后你也可以按照这种方式来编程了呢。写到这里我突然想到了Command模式,Command模式将方法封装成类,Command对象在传递等时候是不会执行任何东西的,只有调用它内部那个方法他才会执行,这样我们就可以把命令到处发,还可以压栈啊等等而不担心在传递过程中Command被处理了,也许这也算是一种延迟计算吧。
本文也只是很浅的谈了一下延迟计算的东西,从这里还可以牵扯到并发编程模型和协同程序等更多内容,由于本人才疏学浅,所以只能介绍到这个地步了,上面一些说法也是我个人理解,肯定有很多不妥地方,欢迎大家拍砖。
foreach,yield,这个我们平常经常使用的东西居然背后还隐藏着这么多奇妙的地方,我也是今天才知道,看来未来的路还很远很远啊。
路漫漫其修远兮,吾将上下而求索。