从表面看,委托似乎很容易使用:用C#的delegate关键字定义,用熟悉的new操作符构造委托实例,用熟悉的方法调用语法来调用函数(用引用了委托对象的变量代替方法名)。
编译器和CLR在幕后做了大量工作来隐藏复杂性。
首先重新审视这一行代码:
看到这行代码后,编译器实际会像下面这样定义一个完整的类:
(ILDasm.exe)
编译器定义的类有4个方法:一个构造器、Invoke、BeginInvoke和EndInvoke。
(所有委托类型都派生自MulticastDelegate)。
重要提示: System.MulticastDelegate派生自System.Delegate,后者又派生自System.Object。是历史原因造成有两个委托类。这是在是令人遗憾---FCL本该只有一个委托类。没有办法,我们对这两个类都要有所了解。即使创建的所有委托类都将MulticastDelegate作为基类,个别情况下仍会使用Delegate类定义的方法处理自己的委托类型。例如,Delegate类的两个静态方法Combine和Remove的签名都指出要获取Delegate参数。由于你创建的委托类型派生自MulticastDelegate,后者又派生自Delegate,所以你的委托类型的实例是可以传给这两个方法的。
这个类的可访问性是private,因为委托在源代码中声明为internal。如果源代码改成使用public可见性,编译器生成的Feedback类也会变成公共类。要注意的是,委托类即可嵌套在一个类型中定义,也会在全局范围中定义。简单地说,由于委托是类,所以凡是能够定义类的地方,都能定义委托。
由于所有委托类型都派生自MulticastDelegate,所以它们继承了MulticastDelegate的字段、属性和方法。在所有这些成员中,有三个非公共字段是最重要的。
字段 | 类型 | 说明 |
_target | System.Object | 当委托对象包装一个静态方法时,这个字段为null。当委托包装一个实例方法时,这个字段引用的是回调方法要操作的对象。换言之,这个字段指出要给实例方法的隐式参数this的值。 |
_methodPtr | System.IntPtr | 一个内部的整数值,CLR用它标识要回调的方法。 |
_invocationList | System.Object | 该字段通常为null。构造委托链时它引用一个委托数组。 |
注意,所有委托都有一个构造器,它获取两个参数:一个是对象引用,另一个是引用了回调方法的整数。但是我们前面代码中传递的是Program.StaticFeedbackToConsole或p.InstanceFeedbackToConsole这样的值。根据我们的编成知识,这似乎没有可能通过编译。
然而,C#编译器知道要构造的是委托,所以会分析源代码来确定引用的是哪个对象和方法。对象引用被传给构造器的object参数,标识了方法的一个特殊IntPtr值(从MethodDef或MemberRef元数据token获得)被传给构造器的method参数。对于静态方法,会为object参数传递null值。在构造器内部,这两个参数分别保存在_target和_methodPtr私有字段中。除此以外,构造器还将_invocationList字段设为null。
所以,每个委托对象实际都是一个包装器,其中包装了一个方法和调用该方法时要操作的对象。