探索.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 _methodPtrAux;

对这三个字段做详细说明

_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就完事儿了,没想到还有这么一番景象。看来很多东西还是不能想当然,亲身尝试一下才知道真实的情况。

以上是小编为您精心准备的的内容,在的博客、问答、公众号、人物、课程等栏目也有的相关内容,欢迎继续使用右上角搜索按钮进行搜索方法
, instance
, 委托
, dotnet
, 一个
指向
ff14雇员探索委托、.net中声明一个委托、.net中委托、.net 委托、vb.net 委托,以便于您获取更多的相关知识。

时间: 2024-09-17 13:31:46

探索.Net中的委托的相关文章

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

废话 我本来以为委托很简单,本来只想简简单单的说说委托背后的东西,委托的使用方法.原本只想解释一下那句:委托是面向对象的.类型安全的函数指针.可没想到最后惹出一堆的事情来,越惹越多,罪过,罪过.本文后面一部分是我在一边用SOS探索一边记录的,写的非常糟糕,希望您的慧眼能发现一些有价值的东西,那我就感到无比的荣幸了. 委托前世与今生 大家可能还记得,在C/C++里,我们可以在一个函数里实现一个算法的骨架,然后在这个函数的参数里放一个"钩子",使用的时候,利用这个"钩子"

C#中的委托和事件(续)

转自 http://www.cnblogs.com/JimmyZhang/archive/2008/08/22/1274342.html 源码下载:http://www.tracefact.net/SourceCode/MoreDelegate.rar C#中的委托和事件(续) 引言 如果你看过了 C#中的委托和事件 一文,我想你对委托和事件已经有了一个基本的认识.但那些远不是委托和事件的全部内容,还有很多的地方没有涉及.本文将讨论委托和事件一些更为细节的问题,包括一些大家常问到的问题,以及事件

C# 中的委托和事件

转自 http://www.cnblogs.com/jimmyzhang/archive/2007/09/23/903360.html  欢迎浏览本文的后续文章: C#中的委托和事件(续)PDF 浏览:http://www.tracefact.net/Document/Delegates-and-Events-in-CSharp.pdf文中代码在VS2005下通过,由于VS2003(.Net Framework 1.1)不支持隐式的委托变量,所以如果在一个接受委托类型的位置直接赋予方法名,在VS

【转】【UNITY3D 游戏开发之七】C# 中的委托、事件、匿名函数、LAMBDA 表达式

本站文章均为 李华明Himi 原创,转载务必在明显处注明:  转载自[黑米GameDev街区] 原文链接: http://www.himigame.com/unity3d-game/1613.html       Unity3d 开发中,常用的莫过于委托和事件了,所以转载一篇相关文章,写的比较详细的,这里分享一下.      对于匿名函数以及Lambda表达式也是非常常用的,这里就直接分享链接,童鞋们自行学习.     匿名函数.Lambda表达式:http://www.cnblogs.com/

link中的委托为什么可以调用静态函数?不是说成员函数内不能直接调用静态函数么?

问题描述 link中的委托为什么可以调用静态函数?不是说成员函数内不能直接调用静态函数么? link中的委托为什么可以调用静态函数?不是说成员函数内不能直接调用静态函数么? 解决方案 说反了吧.是静态函数不能调用成员函数. 解决方案二: 你说反了,是静态函数,不能直接调用成员函数

性能-c#中的委托相关的问题

问题描述 c#中的委托相关的问题 this.Invoke(new Action(delegate(){ })):因为执行次数过多导致程序性能变差,反应迟缓,能否有什么方法能先设置好这个方法所需执行的代码,需要执行该代码的时候传入具体的参数就行. 解决方案 this.Invoke(new Action(delegate(){ })): 这个里面的代码相当于没有多线程,而是在主线程中同步执行.所以,除了更新界面和线程不安全的代码,其余的都放在委托外面. 另外,少用匿名委托,最好写成方法直接传方法名.

Objective-C中的委托(代理)模式

       我个人更喜欢把委托(Delegate)模式称为代理(Proxy)模式.还是那句话,第一次接触代理模式是在Java中接触的,在Java中实现代理模式和接口是少不了的.当时学习Spring的时候用到了接口回调,其实就是实现的委托代理模式.简单的说代理就是把相应的功能交给实现接口的相应的类中来解决.在OC中没有接口该如何实现代理呢?前面的博客中笔者说提了一句,在OC中的协议和Java中的接口极为相似,都是只声明方法而不去实现,方法的实现在OC中交个遵循协议的类,而在Java中方法的实现交

【译】探索 Kotlin 中的隐性成本(第二部分)

本文讲的是[译]探索 Kotlin 中的隐性成本(第二部分), 原文地址:Exploring Kotlin's hidden costs - Part 2 原文作者:Christophe B. 译文出自:掘金翻译计划 本文永久链接:github.com/xitu/gold-m- 译者:Feximin 校对者:PhxNirvana .tanglie 局部函数,空值安全和可变参数 本文是正在进行中的 Kotlin 编程语言系列的第二部分.如果你还未读过第一部分的话,别忘了去看一下. 让我们重新看一下

浅谈C# 中的委托和事件_C#教程

引言 委托 和 事件在 .Net Framework中的应用非常广泛,然而,较好地理解委托和事件对很多接触C#时间不长的人来说并不容易.它们就像是一道槛儿,过了这个槛的人,觉得真是太容易了,而没有过去的人每次见到委托和事件就觉得心里别(biè)得慌,混身不自在.本文中,我将通过两个范例由浅入深地讲述什么是委托.为什么要使用委托.事件的由来..Net Framework中的委托和事件.委托和事件对Observer设计模式的意义,对它们的中间代码也做了讨论. 将方法作为方法的参数 我们先不管这个标题