大家可能知道在C++类实例方法的编译实现中,编译器会自动在方法参数列表的最后添加一个本类指针类型的参数 this,于是我们就可以在方法的实现中通过 this 来引用本类中的成员。很显然, C#中也有 this 关键字,并且在类方法或属性的实现过程中也可以通过 this 来引用本类中的实例化成员。同样的this,同样的使用方法,是不是说明C#的类实例方法中也将隐含了this 参数呢?我的回答是肯定的,即linkC#的类实例方法在编译时将自动在显式参数列表的前面加入一个 this 参数,当然此 this 参数的类型也是本类类型的引用。下面我将通过CIL(公共中间语言)代码简单证明一下我的说法。
如下定义一个类:
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedBlockStart.gif)
![](https://www.cnblogs.com/Images/OutliningIndicators/ContractedBlock.gif)
![](https://www.cnblogs.com/Images/OutliningIndicators/None.gif)
本 MathOpt 类很简单,定义了一个实例化方法Add()和一个静态方法Sub()。编译此类生成相应的程序集文件或库文件,然后通过 ildasm.exe 工具查看 MathOpt 类生成的CIL中间代码如下(本文假定读者会以上操作):
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedBlockStart.gif)
![](https://www.cnblogs.com/Images/OutliningIndicators/None.gif)
![](https://www.cnblogs.com/Images/OutliningIndicators/ContractedBlock.gif)
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedBlockStart.gif)
![](https://www.cnblogs.com/Images/OutliningIndicators/ContractedBlock.gif)
![](https://www.cnblogs.com/Images/OutliningIndicators/None.gif)
以上代码没有什么特别,逻辑也很简单:先将2个传入的参数值入栈,然后执行减操作,并将结果存到一个临时变量,然后返回该值,结束函数。此处需要注意在 CIL中提取参数值的CIL操作码。ldarg.0 表示取第1个参数,并将其入栈,ldarg.n 表示取第n个参数并入栈。
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedBlockStart.gif)
![](https://www.cnblogs.com/Images/OutliningIndicators/ContractedBlock.gif)
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedBlockStart.gif)
![](https://www.cnblogs.com/Images/OutliningIndicators/ContractedBlock.gif)
![](https://www.cnblogs.com/Images/OutliningIndicators/None.gif)
应当看到以上Add()的CIL代码与Sub()的CIL代码基本相似,它们的执行逻辑也基本一样。唯一需要注意的地方就是在实例化方法Add()中取二个参数值时所使用的参数索引不同于Sub()。显然,在Add()中二个参数的索引位置比Sub()中二个参数的索引位置都大1,这说明参数a并不在第1个参数的位置上,而是处于第2个参数位置上,参数b则处于第3个参数位置上。那么是谁做了第1个参数呢?答案就是 this 引用。实例化方法Add()中隐含了第1个参数this,而静态方法Sub()则没有隐含参数。本文不再去证明第1个实例化方法中的参数就是this 引用,只是将结果直接给出,有兴趣的人去证。
问题2:实例化的方法中含有隐含参数this,为何它能和静态方法一样,作为同一个委托类型的不同引用呢?
C++中的函数指针和C#中的委托有着相似的功能,都能代表着不同的实现方法。在C++中, 一般只将函数指针引用全局方法或类的静态方法,努力不去引用类的实例化方法。其中的原因就是因为实例化方法编译后参数列表中会多出一个参数项,这就容易导 致函数指针的声明与所引用到的方法的声明不一致,这个错误在编译时就会给出。当然,类的实例方法也可以以同样的格式被函数指针引用,但这不是本文要讨论 的,有兴趣的人可以参考《COM技术内幕》或《COM本质论》,里面讨论了实现方案。
C++的函数指针使用起来有那么多的限制,可是看过或用过C#委托编程的朋友似乎并没有觉察出那种限制。C#委托是如何避开了这种限制让我们的委托所见即可所得呢?C#委托到类的静态方法与委托到类的实例化方法似乎没有什么区别,下面就分析一下委托的内部实现让我们更明白C#的委托与this关系并不紧密。
接上例,如下定义一个委托,以便让它可以引用我们MathOpt类中的的Add()或Sub()方法:
// 声明委托类型,它可以委托到带2个整型参数并返回整型值的任何方法
public delegate int MathOptDelegate(int a, int b);
同样,编译程序并查看生成的与此委托类型相关的CIL代码如下:
图1 委托类型相关的CIL代码
如上图所示,当声明委托类型 MathOptDelegate 后,编译器会自动将其封装为一个类,并且让其派生自 MulticastDelegate。关于MulticastDelegate和Delegate二个用作委托类型的基类类型本文不详述他们,有兴趣的可查看MSDN。在MathOptDelegate 类中看到有四个方法,其中:
.ctor() 表此类的构造函数
BeginInvoke() 表通过线程池异步调用委托
EndInvoke() 表阻塞获取BeginInvoke()异步调用的返回值
Invoke() 表同步调用委托
以上四个方法中的后三个都是虚方法,它们是由继承Delegate而得来的。我们的委托真正调用方法的过程都是在这三个函数中实现的。本文仅以Invoke()为例阐述一下委托的过程。
假如我们有如下的测试委托的代码:
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedBlockStart.gif)
![](https://www.cnblogs.com/Images/OutliningIndicators/ContractedBlock.gif)
代码1 是声明了一个委托变量,其实质就是定义了一个图1中所示的MathOptDelegate类的一个变量。
代码2 是在堆上创建此类的对象并返回此对象的引用给delegateMathOpt变量。其中参数m.Add就是实际要委托到的方法体。注意此方法是一个实例化的方法。编译器在后台将自动会把此实例化方法所引用的实例对象m赋值到delegateMathOpt 对象内部。我们可以通过delegateMathOpt.Target属性获取对象m的引用。也就是说,在将委托引用到实例化方法的时候,编译器会自动将此实例化方法所牵连上的对象记录下来。以后通过委托变量执行方法的过程将会很简单地变为通过此引用的对象m直接执行方法的过程。
代码3 是通过委托变量执行方法。其实委托变量如此执行方法的操作从CIL代码来看就是直接翻译为:delegateMathOpt.Invoke(1, 2);此句的CIL代码如下:
IL_0014: ldloc.1 // 将delegateMathOpt变量入栈
IL_0015: ldc.i4.1 // 将整型数值 1 入栈
IL_0016: ldc.i4.2 // 将整型数值 2 入栈
IL_0017: callvirt instance int32 '探讨实例方法中的this参数机制'
.MathOptDelegate::Invoke(int32,int32) // 按所传入的参数值执行委托方法
由上易知,委托方法的直接执行不过是障眼法,写法上是delegateMathOpt(1, 2),其实在后台仍然是按delegateMathOpt.Invoke(1, 2) 代码执行。又因为之前记录了方法所引用到的实例对象m, 故在Invoke()方法中就可以直接对 m.Add() 完成委托方法的调用。很明显看到此 Invoke()中并不需要传递MathOpt对象的什么this参数,就可以很简单地实现对实例方法的调用。Invoke()就是对实例方法执行的一个包装。
代码4 是将委托更新到 MathOpt.Sub()方法上,此为简写,它实质翻译为等价的语句:
delegateMathOpt = new MathOptDelegate(MathOpt.Sub);
当将委托引用到类的静态方法时,在delegateMathOpt内部将不登记方法相关的实例对象(此时也没有实例对象可用),委托对象的 Target 属性值为空,即delegateMathOpt.Target == null.
代码5 是通过委托变量执行方法。其内部实现仍如代码3中说明的一样。
结论:C#的类实例方法中仍含有隐含的 this 参数。C#的委托之所以能够统一的对待类实例的方法和类的静态方法,是因为委托内部通过 Invoke()等方法转发了函数调用。并且可以通过委托对象的 Target 属性知道委托当前所引用到的哪个实例对象。若引用的为静态方法,则此 Target 属性将为空值。
参考文献:
1. 《C#与.NET 3.0 高级程序设计(特别版)》([美] Andrew Troelsen 著, 人民邮电出版社)
2. 《COM本质论》([美] Don 著,潘爱民 译, 电子书)
3. 《COM技术内幕》([美] Dale Rogerson著, 杨秀章 等译, 电子书)
测试环境:Windows web server 2008 vs2005