委托内部机制
小弟第一次写文章,还请大侠亲拍指正。(蛋疼的Chrome崩溃了 重新发一遍)
我们通俗点讲委托就是在一个方法中挖了一个坑,这个坑留着调用该方法的时候来填坑(给该委托赋值),这样子就可以做到需求留着调用者来完成,实现代码重用。网上有个比较经典的例子,烧水的一个程序水到达一定的温度的时候,水壶可能鸣笛报警,可能停止烧水,或者其他的可能。这件事是制造水壶的人不可预知,所以在这里挖了一坑,留着谁调用该烧水程序的人来填坑。当你填上鸣笛报警,水温到那个温度的时候就会鸣笛报警,当你填上停止烧水,到那个温度的时候就会停止烧水(其实这地方用事件更好,这里面说只是方便理解)。、
附上代码:
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 6 MyDel del = new MyDel(M1); 7 del += M2; 8 del += M3; 9 Delegate[] dels = del.GetInvocationList(); 10 foreach (var item in dels) 11 { 12 var my = item as MyDel; 13 Console.WriteLine(my()); 14 } 15 Console.WriteLine(del()); 16 Console.Read(); 17 } 18 public static int M1() 19 { 20 return 1; 21 } 22 public static int M2() 23 { 24 return 2; 25 26 } 27 public static int M3() 28 { 29 return 3; 30 } 31 32 }
首先委托是一个数据类型,跟class,enum,struct一样。定义的委托的时候我们可以这样定义:public delegate int MyDel();其中delegate是定义委托的关键字,int是返回类型,MyDel是委托类型名,括号中可以放你需要的参数(这里面我给的无参)。
然后定义委托变量,public MyDel del=M1;其中M1是
public static int M1()
{
return 1;
}
这样就在我们就可以在Main方法中del()就可以调用该方法。但是当我们实现多播委托的时候如果其中有几个方法例如
1 public static int M1() 2 { 3 return 1; 4 } 5 6 public static int M2() 7 { 8 9 return 2; 10 11 } 12 13 public static int M3() 14 { 15 16 return 3; 17 18 }
我们就可以MyDel del = new MyDel(M1);
del+= M2;
del+= M3;
给委托进行赋值。这样调用Console.WriteLine(del())时候我们得到的结果只能是3。那我们怎么得到所有的返回值?
我们可以通过Delegate[] dels = del.GetInvocationList();得到Delegate数组。这样我们通过
foreach (var item in dels) { var my = item as MyDel; Console.WriteLine(my()); }
可以得到每个的返回值。那大家就会问为什么可以这样的呢?
贴上Main方法(前面已经附上了):
1 static voidMain(string[] args) 2 3 { 4 5 MyDel del = new MyDel(M1); 6 7 del += M2; 8 9 del += M3; 10 11 Delegate[] dels = del.GetInvocationList(); 12 13 foreach (var item in dels) 14 15 { 16 17 var my = item as MyDel; 18 19 Console.WriteLine(my()); 20 21 } 22 23 Console.WriteLine(del()); 24 25 Console.Read(); 26 27 }
我们通过反编译可以看到下面的代码:
1 private static void Main(string[] args) 2 { 3 MyDel del = new MyDel(Program.M1); 4 del = (MyDel) Delegate.Combine(del, new MyDel(Program.M2)); 5 del = (MyDel) Delegate.Combine(del, new MyDel(Program.M3)); 6 Delegate[] dels = del.GetInvocationList(); 7 foreach (Delegate item in dels) 8 { 9 Console.WriteLine((item as MyDel)()); 10 } 11 Console.WriteLine(del()); 12 Console.Read(); 13 }
首先我们得理解我们写的所有的委托都是继承自MulticastDelegate(抽象类),而MulticastDelegate又继承自Delegate类。在MulticastDelegate中有个重要的字段:_invocationList,Delegate中有两个重要字段是_target和_methodPtr。
当我们MyDel del = new MyDel(M1);的时候首先他的该委托变量会存放_methodPtr中,而_target指向的是该类型的实例地址,他指向的是Program,_invocationList就是一个空的Delegate数组(null)。
我们通过+=实现委托链的时候,MulticastDelegate会重写Delegate类中的CombineImpl方法(我截取一部分,这里以del+=M2;为基准):
1 //follow代表的是M2 2 protected sealed override Delegate CombineImpl(Delegate follow){ 3 object[] objArray; 4 int num2; 5 //假如follow(M2)为null的时候返回自己 6 if (follow == null) 7 { 8 return this; 9 } 10 //判断是否类型相同 11 if (!Delegate.InternalEqualTypes(this, follow)) 12 { 13 throw new ArgumentException(Environment.GetResourceString("Arg_DlgtTypeMis")); 14 } 15 //将follow转为MulticastDelegate类型,刚刚已经提过如果是单播委托//的时候invocationList为null 16 MulticastDelegate o = (MulticastDelegate) follow; 17 int num = 1; 18 //这时候objArray2就为 null 19 object[] objArray2 = o._invocationList as object[]; 20 if (objArray2 != null) 21 { 22 num = (int) o._invocationCount; 23 } 24 //将自己的invocationList转换为数组,同上得到的也是null 25 object[] objArray3 = this._invocationList as object[]; 26 if (objArray3 == null) 27 { 28 //得到num2结果为2 29 num2 = 1 + num; 30 objArray = new object[num2]; 31 //将自己放到objArray[0]中 32 objArray[0] = this; 33 //将添加的委托放到objArray[1]中 34 if (objArray2 == null) 35 { 36 objArray[1] = o; 37 } 38 //如果增加的委托变量也是多播委托才会走这步 39 else 40 { 41 for (int i = 0; i < num; i++) 42 { 43 objArray[1 + i] = objArray2[i]; 44 } 45 } 46 //返回一个新的委托这里面返回的invocationList存放了两个Delegate数组,数组中每一个代表委托链中的一个委托实例下面的代码 47 return this.NewMulticastDelegate(objArray, num2); 48 }..(后面代码不在写)} 49 //返回一个新的委托 50 internal MulticastDelegate NewMulticastDelegate(object[] invocationList, int invocationCount, bool thisIsMultiCastAlready) 51 { 52 MulticastDelegate delegate2 = Delegate.InternalAllocLike(this); 53 if (thisIsMultiCastAlready) 54 { 55 delegate2._methodPtr = base._methodPtr; 56 delegate2._methodPtrAux = base._methodPtrAux; 57 } 58 else 59 { 60 delegate2._methodPtr = base.GetMulticastInvoke(); 61 delegate2._methodPtrAux = base.GetInvokeMethod(); 62 } 63 delegate2._target = delegate2; 64 delegate2._invocationList = invocationList; 65 delegate2._invocationCount = (IntPtr) invocationCount; 66 return delegate2; 67 }
所以我们可以通过下面图来理解单播委托与多播委托:
这边提一点:单播委托的时候_invocationList为null,而多播委托的时候,_invocationList是一个Delegate[],数组中每一个成员就是一个委托实例。
这里我们的注意的一点是委托跟字符串具有一样的特性:不可变性。我们刚开始定义委托变量MyDel del = new MyDel(M1);堆内存中分配了一块地址当del+=M2;的时候堆内存为M2分配了一块内存地址,而堆为del重写分配了一块地址用于存放del,栈中重新指向了另外一块内存地址。所以一开始的del是没有改变。这时候一开始堆内存的del(M1)跟M2就可以被垃圾回收了。
附上赵晓虎的图:
扯到这不得不提类堆内存的存放。借用我们马伦的画的一张图:
当程序集(dll,exe)加载的时候,在appDomain堆内存中就有一块内存空间用于存放该类型的实例,例如Person类型的实例。当我们Person person=new Person()的时候,在堆内存中一块内存地址由于存放person的变量的空间。他们都会有对象空间,类型指针,同步索引块。我们当前创建的person就会指向person实例的地址,Person就会指向Person类型的实例地址。当前创建的person的类型指针会指向Person类型地址。而Person类型实例中的类型指针会指向自己。
在图中DelDefine del=new DelDefine(person.Show)其中person实例就指向了target,而target就指向了person实例空间地址。methodPtr指向了Show方法。这样我们del()的时候,就是通过person实例调用方法。
好了就说这么多了,欢迎大侠们亲拍指正!!!