购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

3.3 委托的实现

让我们重新定义一个委托并创建它的实例,之后再为该实例绑定一个方法并调用它。

从表面上看,委托似乎十分简单,让我们拆分一下这段代码:用C#中的delegate关键字定义了一个委托类型MyDelegate;使用new操作符来构造一个MyDelegate委托的实例myDelegate,通过构造函数创建的委托实例myDelegate此时所引用的方法是myMethod1,之后我们通过方法组转换为myDelegate绑定另一个对应的方法myMethod2;最后,用调用方法的语法来调用回调函数。看上去一切都十分简单,但实际情况是这样吗?

事实上编译器和Mono运行时在幕后做了大量的工作来隐藏委托机制实现的复杂性。那么本节就要来揭开委托到底是如何实现的这个谜题。

下面让我们把目光重新聚焦在刚刚定义委托类型的那行代码上:

让我们使用Refactor反编译C#程序,可以看到如下的结果:

可以看到,编译器实际上为我们定义了一个完整的类MyDelegate:

可以看到,编译器为我们的MyDelegate类定义了4个方法:一个构造器、Invoke、BeginInvoke以及EndInvoke。而MyDelegate类本身又派生自基础类库中定义的System.MulticastDelegate类型,所以这里需要说明的一点是所有的委托类型都派生自System.MulticastDelegate。但是各位读者可能也会了解到在C#的基础类库中还定义了另外一个委托类System.Delegate,甚至System.MulticastDelegate也是从System.Delegate派生而来,而System.Delegate则继承自System.Object类。那么为何会有两个委托类呢?这其实是C#开发者的遗留问题,虽然所有我们自己创建的委托类型都继承自MulticastDelegate类,但是仍然会有一些Delegate类的方法会被用到。最典型的例子便是Delegate类的两个静态方法Combine和Remove,而这两个方法的参数都是Delegate类型的。

由于我们定义的委托类派生自MulticastDelegate而MulticastDelegate又派生自Delegate,因而我们定义的委托类型可以作为这两个方法的参数。

再回到我们的MyDelegate委托类。由于委托是类,因而凡是能够定义类的地方,都可以定义委托,所以委托类既可以在全局范围中定义,也可以嵌套在一个类型中定义。同样,委托类也有访问修饰符,可以通过指定委托类的访问修饰符如private、internal、public等来限定访问权限。

由于所有的委托类型都继承于MulticastDelegate类,因而它们也继承了MulticastDelegate类的字段、属性以及方法。

需要注意的一点是,所有的委托都有一个获取两个参数的构造方法,这两个参数分别是对对象的引用以及一个IntPtr类型的用来引用回调函数的句柄(IntPtr类型被设计成整数,其大小适用于特定平台。也就是说,此类型的实例在32位硬件和操作系统中将是32位,在64位硬件和操作系统上将是64位。IntPtr对象常可用于保持句柄。例如,IntPtr的实例广泛地用在System.IO.FileStream类中来保持文件句柄)。代码如下:

但是我们回去看一看我们构造委托类型新实例的代码:

似乎和构造器的参数对不上。那为何编译器没有报错,而是让这段代码通过编译了呢?原来C#的编译器知道要创建的是委托的实例,因而会分析代码来确定引用的是哪个对象和哪个方法。分析之后,将对象的引用传递给object参数,而方法的引用被传递给了method参数。如果myMethod1是静态方法,那么object会传递为null。而这两个方法实参被传入构造函数之后,会分别被_target和_methodPtr这两个私有字段保存,并且_invocationList字段会被设为null。

从上面的分析,我们可以得出一个结论,即每个委托对象实际上都是一个包装了方法和调用该方法时要操作的对象的包装器。

接下来我们继续探索如何通过委托实例来调用回调方法。首先我们还是通过一段代码来开启我们的讨论。

编译并且运行之后,输出的结果如下:

我们可以注意到,新定义的Print方法将委托实例作为其中的一个参数,并且首先检查传入的委托实例md是否为null。那么这一步是否是多此一举的操作呢?答案是否定的,检查md是否为null是必不可少的,这是由于md仅仅是可能引用了MyDelegate类的实例,但它也有可能是null,就像代码中的第三种情况所演示的那样。经过检查,如果md不是null,则调用回调方法,不过代码看上去似乎是调用了一个名为md、参数为value的方法:md(value);但事实上并没有一个叫作md的方法存在,那么编译器是如何来调用正确的回调方法的呢?原来编译器知道md是引用了委托实例的变量,因而在幕后会生成代码来调用该委托实例的Invoke方法。换言之,上面刚刚调用回调函数的代码md(value);被编译成了如下的形式:

为了更深一步地观察编译器的行为,我们将编译后的代码反编译为CIL代码,并且截取其中Print方法部分的CIL代码:

分析这段代码,我们可以发现在IL_0008这行,编译器为我们调用了DelegateScript/MyDelegate::Invoke(int32)方法。那么我们是否可以显式地调用md的Invoke方法呢?答案是肯定的。所以,Print方法完全可以改成如下的定义:

而一旦调用了委托实例的Invoke方法,那么之前在构造委托实例时被赋值的字段_target和_methodPtr在此时便派上了用场,它们会为Invoke方法提供对象和方法信息,使得Invoke能够在指定的对象上调用包装好的回调方法。本节讨论了编译器如何在幕后为我们生成委托类、委托实例的内部结构以及如何利用委托实例的Invoke方法来调用一个回调函数,我们接下来继续来讨论一下如何使用委托来回调多个方法。 li1vozbvKyxJHSXOdWItftFoI5//7Pxku61QHH+1PJhRNerNoEg0AH02JE1w40GK

点击中间区域
呼出菜单
上一章
目录
下一章
×