艾伟_转载:探索.Net中的委托

废话

我本来以为委托很简单,本来只想简简单单的说说委托背后的东西,委托的使用方法。原本只想解释一下那句:委托是面向对象的、类型安全的函数指针。可没想到最后惹出一堆的事情来,越惹越多,罪过,罪过。本文后面一部分是我在一边用SOS探索一边记录的,写的非常糟糕,希望您的慧眼能发现一些有价值的东西,那我就感到无比的荣幸了。

委托前世与今生

大家可能还记得,在C/C++里,我们可以在一个函数里实现一个算法的骨架,然后在这个函数的参数里放一个“钩子”,使用的时候,利用这个“钩子”注入一个函数,注入的函数实现不同算法的不同部分,这样就可以达到算法骨架重用的目的。而这里所谓的“钩子”就是“函数指针”。这个功能很强大啊,但是函数指针却有它的劣势:不是类型安全的、只能“钩”一个函数。大家可能都知道微软对委托的描述:委托是一种面向对象的,类型安全的,可以多播的函数指针。要理解这句话,我们先来看看用C#的关键字delegate声明的一个委托到底是什么样的东西:

   1: namespace Yuyijq.DotNet.Chapter2
   2: {
   3:     public delegate void MyDelegate(int para);
   4: }

 

隐藏在背后的秘密

很简单的代码吧,使用ILDasm反编译一下:

奇怪的是,这么简单的一行代码,变成了一个类:类名与委托名一致,这个类继承自System.MulticastDelegate类,连构造器一起有四个成员。看看我们如何使用这个委托:

   1: public class TestDelegate
   2: {
   3:     MyDelegate myDelegate;
   4:  
   5:     public void AssignDelegate()
   6:     {
   7:         this.myDelegate = new MyDelegate(Test);
   8:     }
   9:  
  10:     public void Test(int para)
  11:     {
  12:         Console.WriteLine("Test Delegate");
  13:     }
  14: }

编译后用ILDasm看看结果:

.field private class Yuyijq.DotNet.Chapter2.MyDelegate myDelegate

发现,.Net把委托就当做一个类型,与其他类型一样对待,现在你明白了上面那句话中说委托是面向对象的函数指针的意思了吧。

接着看看AssignDelegate反编译后的代码:

   1: .method public hidebysig instance void  AssignDelegate() cil managed
   2: {
   3:   // Code size       19 (0x13)
   4:   .maxstack  8
   5: //将方法的第一个参数push到IL的运算栈上(对于一个实例方法来说,比如AssignDelegate,它的第一个参数就是“this”了)
   6:   IL_0000:  ldarg.0
   7: //这里又把this压栈了一次,因为下面一条指令中的Test方法是一个实例方法,需要一个this
   8:   IL_0001:  ldarg.0
   9: //ldftn就是把实现它的参数中的方法的本机代码的非托管指针push到栈上,在这里你就可以认为是获取实例方法Test的地址
  10:   IL_0002:  ldftn instance void Yuyijq.DotNet.Chapter2.TestDelegate::Test(int32)
  11: //调用委托的构造器,这个构造器需要两个参数,一个对象引用,就是第一次压栈的this,一个方法的地址。
  12:   IL_0008:  newobj instance void Yuyijq.DotNet.Chapter2.MyDelegate::.ctor(object,native int)
  13:   IL_000d:  stfld class Yuyijq.DotNet.Chapter2.MyDelegate Yuyijq.DotNet.Chapter2.TestDelegate::myDelegate
  14:   IL_0012:  ret
  15: }

通过上面的代码,我们会发现,将一个实例方法分配给委托时,委托不仅仅引用了方法的地址,还有这个方法所在对象的引用,这里就是所谓的类型安全。

我们再回过头来看看MyDelegate的继承链:MyDelegate->MulticastDelegate->Delegate。

奇妙的地方

而Delegate中有三个有趣的字段:

Internal object _target;

Internal IntPtr _methodPtr;

Internal IntPtr _methodPtr;

对这三个字段做详细说明

_target

1、如果委托指向的方法是实例方法,则_target的值是指向目标方法所在对象的指针

2、如果委托指向的是静态方法,则_target的值是委托实例自身

_methodPtr

1、如果委托指向的方法是实例方法,则_methodPtr的值指向一个JIT Stub(如果这个方法还没有被JIT编译,关于JIT Stub会在后面的章节介绍),或指向该方法JIT后的地址

2、如果委托指向的方法是静态方法,则_methodPtr指向的是一个Stub(一段小代码,这段代码的作用是_target,然后调用_methodPtrAux指向的方法),而且所有签名相同的委托,共享这个Stub。为什么要这样一个Stub?我想是为了让通过委托调用方法的流程一致吧,不管指向的是实例方法还是静态方法,对于外部来说,只需要调用_methodPtr指向的地址,但是对于调用实例方法而言,它需要this,也就是这里的_target,而静态方法不需要,为了让这里的过程一直,CLR会偷偷的在委托指向静态方法时插入一小段代码,用于去掉_target,而直接jmp到_methodPtrAux指向的方法。

_methodPtrAux

1、如果委托指向的是实例方法,则_methodPtrAux就是0。

2、如果委托指向的是静态方法,则这时_methodPtrAux起的作用与_mthodPtr在委托指向实例方法的时候是一样的。

实际上通过反编译Delegate的代码发现,Delegate有一个只读属性Target,该Target的实现依靠GetTarget方法,该方法的代码如下:

   1: internal virtual object GetTarget()
   2: {
   3:     if (!this._methodPtrAux.IsNull())
   4:     {
   5:         return null;
   6:     }
   7:     return this._target;
   8: }

实了当委托指向静态方法时,Target属性为null。

我们来自己动手,分析一下上面的结论是否正确。

_target和_methodPtr真的如上面所说的么?何不自己动手看看。

建立一个Console类型的工程,在项目属性的“调试(Debug)”选项卡里选中“允许非托管代码调试(Enable unmanaged code debuging)”。

   1: namespace Yuyijq.DotNet.Chapter2
   2: {
   3:     public delegate void MyDelegate(int para);
   4:     public class TestDelegate
   5:     {
   6:         public void Test(int para)
   7:         {
   8:             Console.WriteLine("Test Delegate");
   9:         }
  10:         public void CallByDelegate()
  11:         {
  12:             MyDelegate myDelegate = new MyDelegate(this.Test);
  13:             myDelegate(5);
  14:         }
  15:  
  16:         static void Main()
  17:         {
  18:             TestDelegate test = new TestDelegate();
  19:             test.CallByDelegate();
  20:         }
  21:     }
  22: }

上面是作为实验的代码。

在CallByDelegate方法的第二行设置断点

F5执行,命中断电后,在Visual Studio的立即窗口(Immediate Window)里输入如下命令(菜单栏->调试(Debug)->立即窗口(Immediate)):

//.load sos.dll用于加载SOS.dll扩展

.load sos.dll

extension C:\Windows\Microsoft.NET\Framework\v2.0.50727\sos.dll loaded

//Dump Stack Objects的缩写,输出栈中的所有对象

//该命令的输出有三列,第二列Object就是该对象在内存中的地址

!dso

PDB symbol for mscorwks.dll not loaded

OS Thread Id: 0x1588 (5512)

ESP/REG Object Name

0037ec10 019928a4 Yuyijq.DotNet.Chapter2.TestDelegate

0037ed50 019928a4 Yuyijq.DotNet.Chapter2.TestDelegate

0037ed5c 019928b0 Yuyijq.DotNet.Chapter2.MyDelegate

0037ed60 019928b0 Yuyijq.DotNet.Chapter2.MyDelegate

0037ef94 019928b0 Yuyijq.DotNet.Chapter2.MyDelegate

0037ef98 019928b0 Yuyijq.DotNet.Chapter2.MyDelegate

0037ef9c 019928a4 Yuyijq.DotNet.Chapter2.TestDelegate

0037efe0 019928a4 Yuyijq.DotNet.Chapter2.TestDelegate

0037efe4 019928a4 Yuyijq.DotNet.Chapter2.TestDelegate

//do命令为Dump Objects缩写,参数为对象地址,输出该对象的一些信息

!do 019928b0

Name: Yuyijq.DotNet.Chapter2.MyDelegate

MethodTable: 00263100

EEClass: 002617e8

Size: 32(0x20) bytes

(E:\Study\Demo\Demo\bin\Debug\Demo.exe)

//该对象的一些字段

Fields:

MT Field Offset Type VT Attr Value Name

704b84dc 40000ff 4 System.Object 0 instance 019928a4 _target

704bd0ac 4000100 8 ...ection.MethodBase 0 instance 00000000 _methodBase

704bb188 4000101 c System.IntPtr 1 instance 0026C018 _methodPtr

704bb188 4000102 10 System.IntPtr 1 instance 00000000 _methodPtrAux

704b84dc 400010c 14 System.Object 0 instance 00000000 _invocationList

704bb188 400010d 18 System.IntPtr 1 instance 00000000 _invocationCount

在最后Fields一部分,我们看到了_target喝_methodPtr,_target的值为019928a4,看看上面!dso命令的输出,这个不就是Yuyijq.DotNet.Chapter2.TestDelegate实例的内存地址么。

在上面的!do命令的输出中,我们看到了MethodTable:00263100,这就是该对象的方法表地址(关于方法表更详细的讨论会在后面的章节介绍到,现在你只要把他看做一个记录对象所有方法的列表就行了,该列表里每一个条目就是一个方法)。现在我们要看看Yuyijq.DotNet.Chapter2.TestDelegate..Test方法的内存地址,看起是否与_methodPtr的值是一致的,那么首先就要获得Yuyijq.DotNet.Chapter2.TestDelegate.的实例中MethodTable的值:

!do 019928a4

Name: Yuyijq.DotNet.Chapter2.TestDelegate

MethodTable: 00263048

EEClass: 002612f8

Size: 12(0xc) bytes

(E:\Study\Demo\Demo\bin\Debug\Demo.exe)

Fields:

None

现在知道了其方法表的值为00263048,然后使用下面的命令找到Yuyijq.DotNet.Chapter2.TestDelegate..Test方法的地址:

!dumpmt -md 00263048

EEClass: 002612f8

Module: 00262c5c

Name: Yuyijq.DotNet.Chapter2.TestDelegate

mdToken: 02000003 (E:\Study\Demo\Demo\bin\Debug\Demo.exe)

BaseSize: 0xc

ComponentSize: 0x0

Number of IFaces in IFaceMap: 0

Slots in VTable: 9

--------------------------------------

MethodDesc Table

Entry MethodDesc JIT Name

.......

0026c010 00262ffc NONE Yuyijq.DotNet.Chapter2.TestDelegate.AssignDelegate()

0026c018 0026300c NONE Yuyijq.DotNet.Chapter2.TestDelegate.Test(Int32)

......

Entry这一列就是一个JIT Stub。看看,果然与_methodPtr的是一致的,因为这时Test方法还没有经过JIT(JIT列为NONE),所以_methodPtr指向的是这里的JIT Stub。

如果给委托绑定一个静态方法呢?现在我们把Test方法改为静态的,那实例化委托的时候,就不能用this.Test了,而应该用TestDelegate.Test。还是在原位置设置断点,使用与上面相同的命令,查看_target与_methodPtr的值。

MT Field Offset Type VT Attr Value Name

704b84dc 40000ff 4 System.Object 0 instance 01e928b0 _target

704bb188 4000101 c System.IntPtr 1 instance 007809C4 _methodPtr

704bb188 4000102 10 System.IntPtr 1 instance 0025C018 _methodPtrAux

你会发现这里的_target字段的值就是MyDelegate的实例myDelegate的地址。然后我们通过上面的方法,找到Test方法的地址,发现_methodPtrAux的值与该值是相同的。

实际上你还可以再编写一个与MyDelegate相同签名的委托,然后也指向一个静态方法,使用相同的方法查看该委托的_methodPtr的值,你会发现这个新委托与MyDelegate的_methodPtr的值是一致的。

刚才不是说这个时候_methodPtr指向的是一个Stub么,既然如此那我们反汇编一下代码:

!u 007809C4

Unmanaged code

007809C4 8BC1 mov eax,ecx

007809C6 8BCA mov ecx,edx

007809C8 83C010 add eax,10h

007809CB FF20 jmp dword ptr [eax]

........

.Net里JIT的方法的调用约定是Fast Call,对于Fast Call来说,方法的前两个参数会放在ECX和EDX两个寄存器中。那么mov eax,ecx实际上就是将_target传递给eax,再看看

704bb188 4000102 10 System.IntPtr 1 instance 0025C018 _methodPtrAux

_methodPtrAux的偏移是10,这里的add eax,10h就是将eax指向_methodPtrAux,然后jmp dword ptr[eax]就是跳转到_methodPtrAux所指向的地址了,就是委托指向的那个静态方法。

通过委托调用方法

如何通过委托调用方法呢:

   1: public void CallByDelegate()
   2: {
   3:    MyDelegate myDelegate = new MyDelegate(this.Test);
   4:  
   5:    myDelegate(5);
   6: }

再来看看其对应的IL代码:

   1: .method public hidebysig instance void  CallByDelegate() cil managed
   2: {
   3:   // Code size       21 (0x15)
   4:   .maxstack  3
   5:   .locals init ([0] class Yuyijq.DotNet.Chapter2.MyDelegate myDelegate)
   6:   IL_0000:  ldarg.0
   7:   IL_0001:  ldftn instance void Yuyijq.DotNet.Chapter2.TestDelegate::Test(int32)
   8:   IL_0007:  newobj instance void Yuyijq.DotNet.Chapter2.MyDelegate::.ctor(object, native int)
   9:   IL_000c:  stloc.0
  10:   IL_000d:  ldloc.0
  11:   IL_000e:  ldc.i4.5
  12:   IL_000f:  callvirt   instance void Yuyijq.DotNet.Chapter2.MyDelegate::Invoke(int32)
  13:   IL_0014:  ret
  14: }

前面的代码我们已经熟悉,最关键的就是

callvirt instance void Yuyijq.DotNet.Chapter2.MyDelegate::Invoke(int32)

我们发现,通过委托调用方法,实际上就是调用委托的Invoke方法。

多播的委托

好了,既然已经解释了面向对象和类型安全,那么说委托是多播的咋解释?

你可能已经发现,MyDelegate继承自MulticastDelegate,看这个名字貌似有点意思了。来看看下面这两行代码:

   1: MyDelegate myDelegate = new MyDelegate(this.Test);
   2: myDelegate += new MyDelegate(this.Test1);

通过IL我们可以发现,这里的+=最后就是调用System.Delegate的Combine方法。而Combine的真正实现时在MulticastDelegate的CombineImpl方法中。在MulticastDelegate中有一个_invocationList字段,从CombineImpl中可以看出这个字段是一个object[]类型的,而委托链就放在这个数组里。

后记

文章是想到哪儿写到哪儿,写的比较乱,也比较匆忙。非常抱歉。对于中间那段奇妙的事情,我原来真的不知道,我一直以为当委托指向一个静态方法时,_target指向null就完事儿了,没想到还有这么一番景象。看来很多东西还是不能想当然,亲身尝试一下才知道真实的情况。

时间: 2024-09-20 16:03:31

艾伟_转载:探索.Net中的委托的相关文章

艾伟_转载:把委托说透(1):开始委托之旅 委托与接口

委托,本是一个非常基础的.NET概念,但前一阵子在园子里却引起轩然大波.先是Michael Tao的随笔让人们将委托的写法与茴香豆联系到了一起,接着老赵又用一系列文章分析委托写法的演变,并告诫"嘲笑孔乙己的朋友们,你们在一味鄙视"茴"的四种写法的同时,说不定也失去了一个了解中国传统文化的机会呢!". 在我个人看来,委托是.NET Framework中一个非常炫的特性,绝不会向有些评论里说的那样,根本没有机会接触.恰恰相反,我们几乎每天都会接触委托,使用委托. 其实园

艾伟_转载:把委托说透(2):深入理解委托

在上一篇随笔中我们通过示例逐步引入了委托,并比较了委托和接口.本文将重点剖析委托的实质. 委托在本质上仍然是一个类,我们用delegate关键字声明的所有委托都继承自System.MulticastDelegate.后者又是继承自System.Delegate类,System.Delegate类则继承自System.Object.委托既然是一个类,那么它就可以被定义在任何地方,即可以定义在类的内部,也可以定义在类的外部. 正如很多资料上所说的,委托是一种类型安全的函数回调机制, 它不仅能够调用实

艾伟_转载:解惑答疑:C#委托和事件

相信很多人一直为C#委托和事件所困惑,尤其是C#的初学者,学到这一部分会感觉比较困难,很有可能就放弃了,而且.NET对委托和事件封装得挺好,一般都不怎么用到自定义的委托和事件,所以放弃学习该技术就有了借口! 网上也有不少此类的文章,最具代表性的是张子阳的C#中的委托和事件这篇,写得的确很好,得到很多读者的赞赏,但我 看评论,还是发现了些问题,因为有不少读者是看了一遍又一遍,每次感觉都蛮好,可是隔一段时间,对"委托和事件"又迷糊了,于是又来看!我真搞不懂,为什 么会出现这种情况!后来想想

艾伟_转载:在Mono中创建ASP.NET程序

一只可爱的猴子: 一次偶然的机会碰到这只猴子,并在工作中也用到它了.现将自己的使用经验分享与此(以OpenSUSE为例介绍). 对于不熟悉Mono的朋友来说,Mono就是.Net在Linux和其它操作系统上的实现(Unix.Mac.iphone.Windows...).Mono的官方网站是:http://www.mono-project.com 严格来讲,Mono是一个开源的.跨平台的C#语言和其CLR的实现,并与微软的.NET二进制兼容.很绕口吧,这是Mono官方网站写的:An open so

艾伟_转载:.NET 4.0中数组的新增功能

1.两数组是否"相等"? 在实际开发中,有时我们需要比对两个数组是否拥有一致的元素,例如,以下两个数组由于拥有相同的元素,因此被认为是相等的: int[] arr1 = new int[]{1,2,3,4};int[] arr2 = new int[]{1,2,3,4}; 在.NET早期版本中,要实现上述数组比对功能,必须自己动手编写一个函数,在其内部使用循环语句逐个比较两个数组的对应元素,才知道这两个数组是否相等. 在.NET 4.0中,数组基类Array实现了一个新增的接口IStr

艾伟_转载:数组排序方法的性能比较(中):Array.Sort<T> 实现分析

昨天我们比较了Array.Sort方法与LINQ排序的性能,知道了LINQ排序的性能以较大幅度落后于Array.Sort方法.而对于Array.Sort来说,性能最高的是其中使用Comparer.Default作为比较器的重载方法.在前文的末尾我们做出了推测:由于排序算法已经近乎一个标准了(快速排序),因此从算法角度来说,Array.Sort方法和LINQ排序上不应该有那么大的差距,因此造成两者性能差异的原因,应该是具体实现方式上的问题. 下载.NET框架的代码 既然是比较实现的区别,那么阅读代

艾伟_转载:VS 2010 和 .NET 4.0 系列之《ASP.NET 4 中的SEO改进 》篇

本系列文章导航 VS 2010 和 .NET 4.0 系列之<ASP.NET 4 中的SEO改进 >篇 VS 2010 和 .NET 4.0 系列之<干净的Web.Config文件 >篇 VS 2010 和 .NET 4.0 系列之<起始项目模板>篇 VS 2010 和 .NET 4.0 系列之<多定向支持>篇 VS 2010 和 .NET 4.0 系列之<多显示器支持>篇 VS 2010 和 .NET 4.0 系列之<代码优化的Web开发

艾伟_转载:VS 2010 和 .NET 4.0 系列之《在ASP.NET 4 Web Forms中实现URL导向》篇

本系列文章导航 VS 2010 和 .NET 4.0 系列之<ASP.NET 4 中的SEO改进 >篇 VS 2010 和 .NET 4.0 系列之<干净的Web.Config文件 >篇 VS 2010 和 .NET 4.0 系列之<起始项目模板>篇 VS 2010 和 .NET 4.0 系列之<多定向支持>篇 VS 2010 和 .NET 4.0 系列之<多显示器支持>篇 VS 2010 和 .NET 4.0 系列之<代码优化的Web开发

艾伟_转载:ADO.NET中的五个主要对象

Connection 物件 Connection 对象主要是开启程序和数据库之间的连结.没有利用连结对象将数据库打开,是无法从数据库中取得数据的.这个物件在ADO.NET 的最底层,我们可以自己产生这个对象,或是由其它的对象自动产生. Command 物件 Command 对象主要可以用来对数据库发出一些指令,例如可以对数据库下达查询.新增.修改.删除数据等指令,以及呼叫存在数据库中的预存程序等.这个对象是架构在Connection 对象上,也就是Command 对象是透过连结到数据源 Data