委托揭秘
——框架设计(第二版):CLR Via C#
参考:
框架设计(第二版):CLR Via C#——15.4 委托揭秘(P281)
正文:
代码1-1,这是一个简单的委托使用。
using System.Collections.Generic;
using System.Text;
namespace Delegate
{
public class DelegateTest
{
protected delegate void MyDelegate();
private void TestMethod() { }
private void Method()
{
MyDelegate aMyDelegate = new MyDelegate(TestMethod);
Method(aMyDelegate);
}
private void Method(MyDelegate aMyDelegate)
{
if (aMyDelegate != null)
{
aMyDelegate();
}
}
}
}
代码1-1
从表面上看,委托似乎很容易使用:用C#的delegate关键字来定义,用我们都熟悉的new操作符来构造委托实例,用我们熟悉的方法调用语法来调用回调函数(不过要用引用了委托对象的变量来代替方法名)。
然而,实际情况比前面几个例子所演示的复杂得多。编译器和CLR做了大量的幕后工作来隐藏复杂性。本节将集中讲解编译器和CLR是如何实现委托的。掌握这些知识有助于我们理解委托,并学会如何更好地使用它们。与此同时,本节还要适当地介绍委托的其它一些特征。
首先重新查看下面这行代码:代码1-2
代码1-2
看到这行代码时,编译器实际上会像下面这样定义一个完整的类:代码1-3
{
//构造器
public MyDelegate(Object aobject,IntPtr method);
//方法的原型与源代码指定的相同
public virtual void Invoke();
//允许异步回调的方法
public virtual IAsyncResult BeginInvoke(
AsyncCallback callback, object aobject);
public virtual void EndInvoke(IAsyncResult result);
}
代码1-3
编译器定义的类有4个方法:一个构造器、Invoke、BeginInvoke和EndInvoke。在本章,要重点解释构造器和Invoke方法。
事实上,可以验证编译器确实会自动生成这个类,具体做法就是用ILDasm.exe来查看生成的程序集(assembly),如图1-1所示。
图1-1 ILDasm.exe 显示了编译器为委托生成的元数据
在这个例子中,编译器定义了一个类,名为MyDelegate,该类继承自 Framework Class Library(FCL)中定义的System.MulticastDelegate 类型(所有委托类型都继承自 MulticastDelegate)。
重要提示: System.MulticastDelegate 类继承自 system.Delegate,后者本身继承自System.Object。之所以有两个委托类,是有历史原因的,同时也是很遗憾的,FCL中本应只有一个委托类。没有办法,我们需要了解这两个类,因为即使构建的所有委托类型都把MulticastDelegate作为基类,我们仍然会在个别情况下用Delegate类(而不是MulticastDelegate类)定义的方法来处理自己构建的这些委托类型。例如,Delegate 类有两个静态方法,分别名为 Combine 和 Remove 。这两个方法的前面指出它们要取 Delegate 参数。因为委托类型继承自 MulticastDelegate,后者又继承自 Delegate,所以委托类型的实例可以被传入这二个方法。
委托类有Public可见性,因为委托在源代码中被声明为Public类。我们应该知道,委托类可以在一个类型内部(即嵌套在另一个类型内)或在全局范围内定义。简单地说,因为委托是类,在可以定义类的任何地方,都可以定义委托。
因为所有委托类型都继承 MulticastDelegate,所以它们都继承了 MulticastDelegate 的字段、属性和方法。在所有这些成员中,有3个非公共字段是最重要的。
字段:_target
类型:System.Object
描述:当委托对象封装一个静态方法时,这个字段为 null。当委托对象封装一个实例方法时,这个字段引用的是调用回调方法是要操作的对象。换而言之,这个字段指名要传给实例方法的隐式 this 参数的值。
字段:_methodPtr
类型:System.IntPtr
描述:一个内部的整数值,CLR用它标识要回调的方法。
字段:_invocationList
类型:System.Object
描述:该字段通常为null。在构建一个委托链时,它可以引用一个委托数组。
注意,所有委托都有一个构造器,该函数取2个参数:一个是对象引用,另一个是引用回调方法的一个整数。然而,如果仔细查看代码1-1的源代码,会发现传递的是 TestMethod。根据我们所学的编程知识判断,这段代码不会通过编译!
然而,C#编译器知道正在构建的是委托,所以会解析源代码以确定引用的是哪个对象和方法。对象引用被传递给构造器的aobject参数,一个特殊的标识方法的 IntPtr值(从MethodDef 或 MemberRef 元数据标记获得)被传递给method 参数。对于静态方法,null 被传递给 object 参数。在构造器内部,这两个参数分别保存在 _target 和 _methodPtr 私有字段内。
除此之外,构造器还将 _invocationList 字段设置为 null。
如此看来,每个委托对象实际封装了一个方法和调用该方法时要操作的一个对象。
知道了委托对象如何构造,并了解了其内部结构之后,我们要谈谈回调方法是如何调用的。 为了方便起见,下面重复了代码1-1中的 Method(MyDelegate aMyDelegate)方法的代码:
{
if (aMyDelegate != null)
{
aMyDelegate();
}
}
if 语句首先检查 aMyDelegate 是否为 null。如果 aMyDelegate 不为 null,你会看到下一行代码调用了回调方法。null 检查必不可少,因为 aMyDelegate 实际上只是一个可能引用 MyDelegate 委托对象的变量,它也可能为 null。这段代码看上去就像是调用了一个名为 aMyDelegate 的函数。但是,这里没有名为 aMyDelegate 的函数。再次重申,因为编译器知道 aMyDelegate 是一个引用了一个委托对象的变量,所以会生成代码调用该委托对象的Invoke方法。换而言之,编译器看到以下代码时:
将生成一下代码,好像源代码本来就是这么写的一样:
为了验证编译器生成代码来调用委托的 Invoke 方法,我们可以用 ILDasm.exe 来检查为 void Method(MyDelegate aMyDelegate) 方法创建的IL代码。图1-2 展示了 void Method(MyDelegate aMyDelegate) 方法的中间语言(intermediate language,IL)代码。
图1-2 ILDasm.exe 证明编译器生成了对 MyDelegate 委托类型的 Invoke 方法的调用
事实上,我们可以修改 void Method(MyDelegate aMyDelegate) 方法来显示调用 Invoke,如下所以:
{
if (aMyDelegate != null)
{
aMyDelegate.Invoke();
}
}
我们还记得编译器在定义 MyDelegate 类的时候定义 Invoke 的。所以在调用 Invoke 的时候,它使用的是 private _target 和 _methodPtr 字段来对指定对象调用所需的方法。注意,Invoke 方法的签名匹配委托的签名,因为 MyDelegate 委托是一个无参数并返回类型为 void,Invoke 方法(如编译器生成的那样)也是一个无参数并返回 void。
End.