这是本系列的第一篇文章,这个系列主要是想和大家分享自己在学习.NET的过程中关于IL语言的一些心得体会。
C#语言在3.0以后,在语言创新上已经上升到了一个新的台阶,扩展方法、匿名函数、Lambda表达式、Linq等等。我们知道,.net framework 3.0/3.5都是基于CLR2.0基础之上的,CLR在功能上并没有任何的提升。上面提到的新的语法特性,在IL语言级别,都会被脱掉“华丽的外衣”,露出其真实的面目——类、类的数据成员、类的成员函数。其他神马都是浮云。
.net程序员是否需要学习IL语言,已经有很多大牛已经讨论过了,这当然取决于个人需求和兴趣,但是个人比较倾向于金旭亮老师的观点:对于一般.net程序员,也有必要了解IL语言。因为阅读IL语言能帮助我们认清楚.net程序在编译时编译器暗地里搞了什么鬼,也能帮助我们认清楚C#,VB.NET语言多态机制的实现原理等等,这有助于我们写出更好的代码。
总之、通过分析IL语言,我们可以让C#这些新语法特性,变得不再那么神秘。
Linq查询有一个很重要的特性就是“延迟执行”,即在代码第一次试图遍历该查询结果的时候才执行。
为了搞清楚.net如何实现该机制,我做了如下实验。
下面是一个很简单的静态函数:
static void MyMethod() { int[] arr = { 1, 3, 4, }; var query = arr.Where(num => num > 1); foreach (var num in query) { Console.WriteLine(num); } }
这个函数创建了一个数组对象,建立一个Linq查询,并且遍历查询结果。
下面我们来看看这个函数所对应的IL语言。
我们看到,对应于上面的MyMethod函数,C#编译器新做了如下工作:
- MyMethod成员函数。
- 增加了一个静态的Func<Int32, bool>类型的成员CS$<>9__CachedAnonymousMethodDelegate1;
- 增加了一个静态成员函数<MyMethod>b__0,该函数接受一个Int32类型参数,返回bool类型;
我们注意到CS$<>9__CachedAnonymousMethodDelegate1的签名同函数<MyMethod>b__0吻合,可以推断两者一定会有关联。
<MyMethod>b__0
该函数的IL代码如下:
.method private hidebysig static bool '<MyMethod>b__0'(int32 num) cil managed { .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) // Code size 5 (0x5) .maxstack 8 IL_0000: ldarg.0 // 将0位置参数压入栈 IL_0001: ldc.i4.1 // 将常整数1压入栈 IL_0002: cgt // 比较栈中两数大小,并将结果压入栈 IL_0004: ret } // end of method Program::'<MyMethod>b__0'
通过Reflector工具得到的C#代码如下:
[CompilerGenerated] private static bool <MyMethod>b__0(int num) { return (num > 1); }
这个函数逻辑很简单——如果参数大于1,返回true,否则返回false;
MyMethod
该函数IL代码相对复杂,为了简便,只列出关键IL代码:
.method private hidebysig static void MyMethod() cil managed { // Code size 97 (0x61) .maxstack 4 .locals init ([0] int32[] arr, [1] class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> query, [2] int32 num, [3] class [mscorlib]System.Collections.Generic.IEnumerator`1<int32> CS$5$0000) ... // 初始化数组arr IL_0013: ldsfld class [mscorlib]System.Func`2<int32,bool> ILDiscovery.Program::'CS$<>9__CachedAnonymousMethodDelegate1' // 将CS$<>9__CachedAnonymousMethodDelegate1的值压入计算堆栈 IL_0018: brtrue.s IL_002b // 如果CS$<>9__CachedAnonymousMethodDelegate1非空,则跳转到语句IL_002b上 IL_001a: ldnull // 将空指针压入栈 IL_001b: ldftn bool ILDiscovery.Program::'<MyMethod>b__0'(int32) // 将指向<MyMethod>b__0方法的函数指针压入栈 IL_0021: newobj instance void class [mscorlib]System.Func`2<int32,bool>::.ctor(object, native int) // 创建一个新的类型Func<Int32, bool>的对象 IL_0026: stsfld class [mscorlib]System.Func`2<int32,bool> ILDiscovery.Program::'CS$<>9__CachedAnonymousMethodDelegate1' // 将上面创建的对象赋值给CS$<>9__CachedAnonymousMethodDelegate1 IL_002b: ldsfld class [mscorlib]System.Func`2<int32,bool> ILDiscovery.Program::'CS$<>9__CachedAnonymousMethodDelegate1' // 将CS$<>9__CachedAnonymousMethodDelegate1的值压入堆栈 IL_0030: call class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0> [System.Core]System.Linq.Enumerable::Where<int32>(class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0>, class [mscorlib]System.Func`2<!!0,bool>) // 调用System.Linq.Enumerable下的静态方法Where(...) IL_0035: stloc.1 // 将上面调用Where函数的结果赋值给位置1的局部变量query IL_0036: ldloc.1 // 将位置为1的局部变量query压入栈 IL_0037: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1<!0> class [mscorlib]System.Collections.Generic.IEnumerable`1<int32>::GetEnumerator() // 调用IEnumerator的接口方法GetEnumerator() IL_003c: stloc.3 // 将结果赋值给位置3的 CS$5$0000 /* 通过枚举器遍历元素并调用Console.WriteLine()函数输出结果 */ .try { IL_003d: br.s IL_004c IL_003f: ldloc.3 IL_0040: callvirt instance !0 class [mscorlib]System.Collections.Generic.IEnumerator`1<int32>::get_Current() IL_0045: stloc.2 IL_0046: ldloc.2 IL_0047: call void [mscorlib]System.Console::WriteLine(int32) IL_004c: ldloc.3 IL_004d: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext() IL_0052: brtrue.s IL_003f IL_0054: leave.s IL_0060 } // end .try finally { IL_0056: ldloc.3 IL_0057: brfalse.s IL_005f IL_0059: ldloc.3 IL_005a: callvirt instance void [mscorlib]System.IDisposable::Dispose() IL_005f: endfinally } // end handler IL_0060: ret } // end of method Program::MyMethod
通过这段IL指令,我们证实了上面的推断:CS$<>9__CachedAnonymousMethodDelegate1变量用来存储<MyMethod>b__0函数本地代码的地址。
另外我们还能得出如下结论:
- 所谓扩展方法,实际上并没有修改“被扩展的类型”,而是编译器在发现对扩展方法的调用时,自动的将其编译为调用“定义该扩展方法的类型所对应的静态方法”;
- 所谓Lambda表达式,就像是匿名函数的简化版本,编译器在编译为IL语言时,会生成一个对应的成员函数。
但是,现在通过以上的内容,我们没有看到任何能够说明Linq查询“延迟执行”的东西,因为在第IL_0030句指令返回了一个IEnumerable<>类型的对象后,在接下的指令中,通过获取该对象的枚举器(IL_0037),然后都是正常遍历一个集合对象的代码。
“延迟执行”究竟是如何实现的??
通过Reflector(不得不说,这玩意儿太好用了)
public override bool MoveNext() { if (base.state == 1) { while (this.index < this.source.Length) { TSource arg = this.source[this.index]; this.index++; if (this.predicate(arg)) { base.current = arg; return true; } } this.Dispose(); } return false; }
这段代码就是以上MoveNext函数的具体实现,程序实际上是在每次执行MoveNext的时候,才调用this.predicate(arg)函数,而我们之前已经将条件判定函数<MyMethod>b__0指针同过Where方法传给了返回的IEnumerable<int>对象。
这就是Linq“延迟执行的奥秘”——在实际遍历的时候才执行条件判定函数!