.NET学习笔记(九) ——委托(下)

这一篇主要介绍委托的一些内部结构,想要了解委托的使用和功能可以看前一篇文章:.NET学习笔记(八) ——委托(上)

前天不小心感冒了,吃了药但还是晕晕的,还是继续写把。。。前面介绍了委托的功能和使用方法。委托实际是对一个函数指针的封装,那么他是如何指向一个函数,有是如何把多个方法都绑定起来的呢?

 

一 委托的内部结构

 

下面是简单的一个定义委托方法,写起来很简单,当编译器编译这句话的时候做了什么呢?

//申明一个委托要绑定的方法签名
public deletgate void ExampleDeletgate(int x);

通过ILDASM我们可以看到,系统产生了以下代码:

//构造器有2个参数为了方便后面使用,我重新定义这2个变量名字
//public ExamplDelegate(object target,int methodPtr)
.method public hidebysig specialname rtspecialname  
       instance void  .ctor(object 'object', native int 'method') runtime managed
{
}

//下面两个是委托的一个异步回调用方法,我们暂时不关注他们
.method public hidebysig newslot virtual 
        instance class [mscorlib]System.IAsyncResult 
        BeginInvoke(int32 x,class [mscorlib]System.AsyncCallback callback,object 'object') runtime managed
{
}

.method public hidebysig newslot virtual 
        instance void  EndInvoke(class [mscorlib]System.IAsyncResult result) runtime managed
{
}

//这个方法和委托指定的方法原形是一样的,我同样改写下
//public void virtual Invoke(int32 x);
.method public hidebysig virtual instance void Invoke(int32 x) runtime managed
{
}

上面的代码有点乱,但是应该还是可以看出点头绪来。编译器在编译到这句的时候实际是产生了一个名字为ExampleDelegate的类,并且生成了4个方法。其中第一个是一个类型构造器,他需要2个参数。看到我重新命名的参数,大家应该猜测到他的作用了把。最后一个是一个Invoke方法,这个词是调用的意思。而且他的类型和申明的方法原形签名是相同的。大家也应该可以猜到。委托实际调用的是这个方法。而另外2个方法是和Invoke没什么区别,只是异步调用。后面文章会介绍异步调用的知识。

上面说了编译器的工作,下面我们具体来看下这个类,和其中的Invoke方法。ExampleDelegate成为了一个类,还记得定义委托对象是如何做的吗?

ExampleDelegate ed1= new ExampleDelegate(Class.method) //实例方法
 
ExampleDelegate ed2 = new ExampleDelegate(method) //静态方法

在定义一个ExampleDelegate类型的对象的时候,我们给他传递了一个方法。这里大家肯定觉得奇怪,上面的构造方法可是需要2个参数的。我们先看看这2个参数,target 是一个object对象,他表示帮定方法的对象。而method则是表示绑定的方法。编译的时候一个参数确实也通过了。这是因为编译器知道我们在构造一个委托。所以他会自动分析我们传递给他的方法的对象。

ExampleDelegate类中有3个私有的成员变量,其中前两个提供了Target和Method 2个属性。我们可以通过这2个属性判断委托是否与一个方法绑定。

看完了上面的3个字段应该就明白了,在定义一个委托的时候,我们的委托对象已经在内部记录了绑定的对象和要执行的方法。下面看下通过委托调用这个方法的情况

object _target;   //指向绑定方法的对象,如果是实例方法则为调用方法的对象,若为静态方法则为空


MulticaseDelegate  _prev ;  //这也是一个委托类型,用与之向下一个委托,初始为null,在.net2.0中这个字段是object类型
int _method,     //这个字段是从元数据中获得的一个值,改值是绑定的方法在元数据中的一个标识值

看到编译后的代码实际是调用了内部的Inovke函数。当调用这个方法时,会用到_target和_method字段,这就让代理在指定的对象上调用期望的方法。

ed1(x); //通过委托调用绑定的方法

//下面是ILDASM中看到的
 IL_000c:  stloc.0
 IL_000d:  ldloc.0
 IL_000e:  ldc.i4.3
 IL_000f:  callvirt   instance void delegateEx.Class1/ExampleDelegate::Invoke(int32)  //实际上是调用ExampleDelegate对象的Invoke方法
 IL_0014:  ret

 

 

二 委托链

 

 

在前一片文章中,我们提到过委托链这个概念,而上面内部结构中有个_prev字段就是用来指向下一个委托对象的。这就使得委托可以指向其他对象,组成一个链表结构。

在Delegate类中提供了几个静态方法,来往一个委托链上添家或移除委托对象。

public static Delegate Combine(Delegate a, Delegate b){}

public static Delegate Combine(params Delegate[] delegates){ }

public static Delegate Remove(Delegate source, Delegate value){ }
 
public static Delegate RemoveAll(Delegate source, Delegate value){ }

下面是操作的代码:

ExampleDelegate ed1= new ExampleDelegate(method1) 
 
ExampleDelegate ed2 = new ExampleDelegate(method2) 
ExampleDelegate ed3 = new ExampleDelegate(method3) 

//把2个委托组成串
ExampleDelegate ed = (ExampleDelegate)Delegate.Combine(ed1,ed2);

ed = (ExampleDelegate)Dleegati.Combine(ed,ed3);

要注意这里ed相当于链表的头接点,而我们插入的时候使用的是头插发,就是插入到头节点后面,而不是链尾巴。所以上面最后的结果是ed–> ed3–>ed2–>ed1–>null.委托链执行的时候仍旧是执行各个委托对象内部记录的方法,只是按顺序执行。下面就看下更具体的Invoke方法

public void Invoke(int x )
{
  if (_prve != null) _prve.Invoke(x);  //如果委托后面还有其他委托对象则执行下一个对象的Invoke方法

  _target.method(x);  //如果委托后面没有其他委托对象,则通过内部的2个字段来执行期望的方法
}

可以看到Invoke内部是这样一个样子,所以实际上,方法是从委托链的末尾开始执行的。但要注意的是,如果委托的方法有返回值,那么只有最后一个执行的委托的值会被返回,而其他的委托的值都被丢弃。如何返回所有委托的值在后面会有介绍。

这里要注意的是当使用combine方法时,会构造一个新的委托对象,他的_target,_method字段和原委托相同,但_prve被重新设置。比如Combine(ed1,ed2);ed2会新建立一个委托对象来添加到委托链上,他的_prve指向ed1。因为委托对象一旦被构造就认为是恒定不变的,_prev =  null,不会改变。

 

关于委托比较:

所以2个ed2指向的不是同一个对象,通过ReferenceEquals方法可以看出来。这里简单介绍下委托对象的比较,委托MulticastDelegate类重写了Equals方法。在这个方法中,是通过比较委托对象的3个私有字段判断的,只有这3个字段全部相同时才相等。而在Delegate类中重写的Equals只比较了_target,_method2个字段。

下面看下把委托从委托链移除的例子

ExampleDelegate ed1= new ExampleDelegate(method1) 
 
ExampleDelegate ed2 = new ExampleDelegate(method2) 

//把2个委托组成串
ExampleDelegate ed = (ExampleDelegate)Delegate.Combine(ed1,ed2);

//移除
//方法1:
//ExampleDelegate ed = (ExampleDelegate)Delegate.Remove(ed1,ed2);

//方法2:
ExampleDelegate ed = (ExampleDelegate)Delegate.Remove(ed1,new ExampleDelegate(method2) );

上面有2种方法,但第一种被注释掉了,因为他是错误的,而应该使用第2重,这看起来有些奇怪。但要记得前面说了委托的恒定性,委托一旦建立,他的_prev字段是无法修改的。所以如果你使用了第一种方法,那么ed2的_prev对象还是指向ed1的。所以,我们必须构建一个新的ed2对象,并且他的_prev =null,这样才是把ed2从委托链上移除了。

在移除过程中,Remove方法会扫描委托链,然后比较委托链上是否存在和新建的问题相同的委托。这里进行比较使用的是Delegate.Equals()方法,因为他只比较_target和_method两个字段(框架程序设计中译者写这里不是使用Delegate.Equals,而是使用的一个内部方法比较,但确实只比较这2个字段,我用Reflector看了下,方法如下:)

protected virtual Delegate RemoveImpl(Delegate d)
{
    if ((this._target == d._target) && this.Method.Equals(d.Method))
    {
        return null;
    }
    return this;
}

如果发现相同的委托,则移除这个委托,方法是修改这个委托前一个对象的_prev指针。如果找不到他也不会进行任何操作也不会返回异常。而remove每次只能移除一个委托对象。如果委托链上有2个ed2委托对象,必须使用两次remove。

注意:.net2.0中的委托链有所区别

在2.0中,委托有了一定的修改,上面介绍的是1.1的。但基本都是相同的,只是委托中的_prev字段发生了变化。在2.0中,这个字段成了Object类型,他不在是负责指向下一个委托,而是负责一个委托对象数组,通过这个数组来访问委托对象。

 

.net1.1中的委托链 (ed2,ed3应该指向新建构造的ed2,ed3两个对象) 

.net2.0中的委托链

 

从上面的图就可以看到双方的区别的,在2.0中_prev不在直接指向下一个委托对象,而是通过控制一个数组来实现。建立了ed他指向空,当把ed1加到链上时,直接用ed–>ed1,当把ed2加到链上时,发现已经有了一个委托对象,于是建立一个新的委托对象,并初始化他的3个字段,而他的_prev字段是一个对象数组,_prev[0]指向ed1,而_prev[1]指向ed2。当把ed3加到链上时,_prev一共指向3个对象了。ed指向新的委托对象。而之前的委托对象会被垃圾回收器回收掉。这样的改进,访问时不需要遍历整个链,只需要遍历头结点的_prev数组。为什么怎么改进,我也不太清楚。

在C#中可以直接使用 +=,-=来操作委托链。前面的示例中就可以看到了。因为C#中这2个操作符号进行了重载,可以通过ILDASM工具查看。另外在.net2.0中c#还提供了一些使用委托上的便利,比如匿名方法,这里就不在详细说明了。

 

 

三 对委托进行更多的控制

 

 

从前面可以看到,在访问委托链的时候,都是遍历链,按照顺序来访问,如果其中一个委托发生错误,就会影响后面的委托对象。而且只返回最后一个委托对象的值。当然我们可以自己来显式的访问每个委托对象,而访问之前,我们必须获得链上所有的委托对象。因为委托的_prev是一个私有字段,所以我们想操作一个链表一样来访问每个对象,但可以使用MulticastDelegate提供的一个获得链中的所有委托

//获得一个委托数组,每个元素是ed链中的委托
Delegate arrayofDelegate = ed.GetInvocationList();

foreach(ExampleDelegate eds in arrayofDelegate )
{
    eds(10); //显示的调用委托,可以通过控制语句进行其他控制
}

这样,当有委托发生异常时,可以进行处理,保证其他委托正常运行,而且可以获得每个委托返回的值(如果有返回值)

 

关于事件中的委托:

时间和委托是联系最紧密的,前面我们也说了,事件模型,是通过委托来实现的。消息发布者和订购者都是通过委托来联系的。但是不同的是,在定义委托对象时,加入了event 关键字。这个关键字使得委托的对象在创建被定义为了私有类型,也就是不能象这里,直接对委托对象进行操作。而只能使用+=,-=两个重载的操作符,来操作委托链。这样就避免的对委托的错误操作。其实这个也可以看做是对委托的一种控制。

 

 

四 总结

 

 

通过上面的讲解,对委托的内部结构和实现应该有了一个比较清楚的认识。委托实际是对一个函数指针的封装,通过一个链,把多个委托连接起来。就好象链表一样。委托的用途很广泛,无论是事件,回调函数还是异步编程。而且使用委托也是降低模块间耦合性的一个办法。当然还可以通过接口等其他方法来实现其中一些功能。所以理解委托是十分有必要的。自己也只是了解了一些以后,写成笔记和大家分享,有很多东西也不知道,所以无法说的太深,有任何错误和不恰当的地方也希望大家指出。


如果本文对您有帮助,可以扫描下方二维码打赏!您的支持是我的动力!
微信打赏 支付宝打赏

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注