[关闭]
@callofduty890 2023-02-09T17:32:13.000000Z 字数 5514 阅读 185

委托的本质

超人视觉


本章要讨论回调函数。回调函数是一种非常有用的编程机制,它的存在已经有很多年了。Microsoft.NET Framework通过委托来提供回调函数机制。不同于其他平台(比如非托管C++)
的回调机制,委托的功能要多得多。例如,委托确保回调方法是类型安全的(这是CLR最重要的目标之一)。委托还允许顺序调用多个方法,并支持调用静态方法和实例方法。

初识委托

C“运行时”的qsot函数获取指向一个回调函数的指针,以便对数组中的元素进行排序。在Microsoft Windows中,窗口过程、钩子过程和异步过程调用等都需要回调函数。在.NEI Framework中,回调方法的应用更是广泛。例如,可以登记回调方法来获得各种各样的通知,例如未处理的异常、窗口状态变化、菜单项选择、文件系统变化、窗体控件事件和异步操作已完成等。

在非托管C/C++中,非成员函数的地址只是一个内存地址。这个地址不携带任何额外的信息,比如函数期望收到的参数个数、参数类型、函数返回值类型以及函数的调用协定。简单地说,非托管C/C++回调函数不是类型安全的(不过它们确实是一种非常轻量级的机制。

NET Framework的回调函数和非托管Windows编程环境的回调函数一样有用,一样普遍。
但是,NET Framework提供了称为委托的类型安全机制。
为了理解委托,先来看看如何使用它。以下代码演示了如何声明、创建和使用委托:

用委托调静态方法
示例:

用委托调实例方法
示例:

委托揭秘

从表面看,委托似乎很容易使用:用C#的delegate关键字定义,用熟悉的new操作符构造委托实例,用熟悉的方法调用语法来调用回调函数(用引用了委托对象的变量替代方法名)。

但实际情况比前几个例子演示的要复杂一些。编译器和CLR在幕后做了大量工作来隐藏复杂性。本节要解释编译器和CLR如何协同工作来实现委托。掌握这些知识有助于加深对委托的理解,并学会如何更高效地使用。另外,还要介绍通过委托来实现的一些附加功能。

首先重新审视这一行代码:

  1. internal delegate void FeedbackInt32 value);

看到这行代码后,编译器实际会像下面这样定义一个完整的类:

  1. internal class Feedback:System.MulticastDelegate
  2. {
  3. //构造器
  4. public Feedbackobject objectIntptr method);
  5. //这个方法的原型和源代码指定的一样
  6. public virtual void InvokeInt32 value);
  7. //以下方法实现对回调方法的异步回调
  8. public virtual IAsyncResult BeginInvokeInt32 valueAsyncCallback callbackobject Bobject);
  9. //结束的回调函数
  10. public virtual void EndInvoke IAsyncResult result);
  11. }

编译器定义的类有4个方法:一个构造器、Invoke、BeginInvoke和EndInvoke。本章重点解释构造器和Invoke。BeginInvoke和EndInvoke方法。

事实上,可用LDasm.exe查看生成的程序集,验证编译器真的会自动生成这个类
如图所示:
image_1gil4i69o5f5g5e1apn1suq1m9j9.png-67.7kB

在本例中,编译器定义了Feedback类,它派生自FCL定义的System.MulticastDelegate类型(所有委托类型都派生自MulticastDelegate)。

重要提示
System.MulticastDelegate派生自System.Delegate,后者又派生自System.Object.是历史原因造成有两个委托类,这实在是令人遗憾FCL本该只有一个委托类。没有办法,我们对这两个类都要有所了解。即使创建的所有委托类型都将MulticastDelegate作为基类,个别情况下仍会使用Delegate类(而非MulticastDelegate类)定义的方法处理自己的委托类型。例如,Delegate类的两个静态方法Combine和Remove(后文将解释其用途)的签名都指出要获取Delegate参数。由于你创建的委托类型派生自MulticastDelegate,后者又派生自Delegate,所以你的委托类型的实例是可以传给这两个方法的。

这个类的可访问性是private,因为委托在源代码中声明为internal。如果源代码改成使用public可见性,编译器生成的Feedback类也会变成公共类。要注意的是,委托类既可嵌套在一个类型中定义,也可在全局范围中定义。简单地说,由于委托是类,所以凡是能够定义类的地方,都能定义委托。

由于所有委托类型都派生自MulticastDelegate,所以它们继承了MulticastDelegate的字段属性和方法。在所有这些成员中,有三个非公共字段是最重要的。

MulticastDelegate的三个重要的非公共字段

字段 类型 说明
_target System.Object 当委托对象包装一个静态方法时,这个字段为NULL。当委托对象包装一个实例方法时,这个字段引用的是回调方法要操作的对象。换言之,这个字段指出要传给实例方法的隐式参数this的值
_methodPtr System.IntPtr 一个内部的整数值,CLR用它标识要回调的方法
_invocationList System.Object 该字段通常为null。构造委托链时它引用一个委托数组

注意,所有委托都有一个构造器,它获取两个参数:一个是对象引用,另一个是引用了回调方法的整数。但如果仔细查看前面的源代码,会发现传递的是Program.FeedbackToConsole或p.FeedbackToFile这样的值。根据迄今为止学到的编程知识,似乎没有可能通过编译!

然而,C#编译器知道要构造的是委托,所以会分析源代码来确定引用的是哪个对象和方法。对象引用被传给构造器的object参数,标识了方法的一个特殊IntPtr值(从MethodDef或MemberRef元数据token获得)被传给构造器的method参数。对于静态方法,会为object参数传递null值。在构造器内部,这两个实参分别保存在target和methodPtr私有字段中。除此以外,构造器还将invocationList字段设为null。

所以,每个委托对象实际都是一个包装器,其中包装了一个方法和调用该方法时要操作的对象。例如,在执行以下两行代码之后:

  1. Feedback fbstatic new FeedbackProgram.FeedbackToConsole
  2. Feedback fbInstance new Feedbacknew Program().FeedbackToFile);

fbStatic和fbInstance变量将引用两个独立的、初始化好的Feedback委托对象

如图所示:
image_1gil5bgfc1if11l41d1j1ir2odem.png-53.5kB

所以会生成代码调用该委托对象的Invoke方法。也就是说,编译器在看到以下代码时:

  1. fb(val);

它将生成以下代码,好像源代码本来就是这么写的一样:

  1. fb.Invoke(val);

为了验证编译器生成代码来调用委托类型的nvoke方法,可利用LDasm.exe检查为Counter方法创建的IL代码。下面列出了Counter方法的IL代码。LO0O9处的指令就是对Feedback的Invoke方法的调用。
image_1gil5ir08qj736g1h8jagu10or13.png-130.2kB

其实,完全可以修改Counter方法来显式调用Invoke方法,前面说过,编译器是在定义Feedback类的时候定义Invoke的。在Invoke被调用时,它使用私有字段_target和methodPtr在指定对象上调用包装好的回调方法。

以伪代码的形式,Feedback的Invoke方法基本上是像下面这样实现的:
image_1gil63m4974s52516s4dlph261g.png-156.3kB

  1. public void InvokeInt32 value
  2. {
  3. Delegate[] delegateset = _invocationList as Delegate[];
  4. if delegateSet null
  5. {
  6. //这个委托数组指定了应该调用的委托
  7. foreach Feedback d in delegateSet
  8. {
  9. d(value);//调用每个委托
  10. }
  11. }
  12. else
  13. {
  14. //否则就不是委托链,该委托标识了要回调的单个方法
  15. methodptr.Invoketargetvalue);
  16. //上面这行代码接近实际的代码,实际发生的事情用C#是表示不出来的
  17. }
  18. }

注意,还可调用Delegate的公共静态方法Remove从链中删除委托。

  1. public Int32 InvokeInt32 value
  2. {
  3. Int32 result;
  4. Delegate[] delegateset invocationList as Delegate[]
  5. if delegateset != null
  6. {
  7. //这个委托数组指定了应该调用的委托
  8. foreach Feedback d in delegateset
  9. {
  10. //调用每个委托
  11. result=dvalue);
  12. }
  13. }
  14. else
  15. {
  16. //否则就不是委托链,该委托标识了要回调的单个方法,在指定的目标对象上调用这个回调方法
  17. result methodptr.Invoketargetvalue);
  18. //上面这行代码接近实际的代码
  19. }
  20. }

数组中的每个委托被调用时,其返回值被保存到result变量中。循环完成后,result变量只包含调用的最后一个委托的结果(前面的返回值会被丢弃),该值返回给调用Invoke的代码。

C#对委托链的支持

为方便C#开发人员,C编译器自动为委托类型的实例重载了+=和-=操作符。这些操作符分别调用Delegate.Combine和Delegate.Remove。可用这些操作符简化委托链的构造。ChainDelegateDemol和ChainDelegateDemo2方法生成的L代码完全一样,唯一的区别是ChainDelegateDemo2方法利用C#的+=和-=操作符简化了源代码。

要想证明两个方法生成的L代码一样,可利用ILDasm.exe查看生成的L代码。会看到C#编译器用Delegate类型的Combine和Remove公共静态方法调用分别替代了+-和-操作符。

委托的本质

首先,创建委托会令编译器隐式地创建一个密封类,这个类继承System.MulticastDelegate,后者再继承自System.Delegate。
这个类的成员是固定的,见图8-1。

这个密封类包括1个构造函数和3个核心函数,Invoke方法赋予其同步访问的能力,Beginlnvoke、Endlnvoke赋予其异步访问的能力。这些函数的输入和输出也都是固定的,例如接受一个整型输入,且没有返回值的委托CallBack的3个核心函数:

Invoke方法的参数和返回值同委托本身相同,BeginInvoke的返回值总是IAsyncResult,输入则除了委托本身的输入之外,还包括了AsyncCallback(回调函数)和一个objectoEndInvoke的输入总是IAsyncResult,加上委托中的out和ref(如果有的话)类型的输入,输出类型则是委托的输出类型。

在事件中,委托是事件的发起者sender将EventArgs传递给处理者的管道。所以委托是一个密封类,没有继承的意义。

System.MulticastDelegate 与 System.Delegate

System.MulticastDelegate是所有委托的父类,它含有一个非常重要的字段:—invocationList,指的是委托自身的方法链。
System.Delegate是System*MulticastDelegate的父类,它含有2个非常重要的字段: _target和_methodPtr。其中,如果委托指向了一个实例方法,_target将会等于该实例本身。在方法中,可以通过this访问。如果委托指向了一个静态方法,_target将会等于null。而无论委托指向什么方法,methodPtr都是CLR用于标示委托指向方法的IntPtr值。

在委托的构造函数中,会传人一个object对象和一个IntPtr值。其中,IntPtr值就是你要指向方法本身的元数据的标识( token),所以,构造函数会将委托类中的—me协odPtr设置为传人的IntPtr值,这就把委托和指向的方法连接起来了。所以,委托也可以看成是一种函数指针(它确实包括了一个IntPtr)。当然,它是一个类型安全的函数指针。

我们看看刚才的代码中,创建委托实例时究竟发生了什么:

  1. var cb = new CallBack (CallBackStatic) ;

对应的IL代码:
我们发现,IL- 0019中调用了委托的构造函数,并传人了obj ect对象(它为null)和一个IntPtr值(对应CallBackStatic)。这便证实了我们的判断。而在下面的代码中,object对象将为p本身,IntPtr值对应CallBackInstance。

  1. var p = new Program();
  2. var cb2 = new CallBack (p.CallBackInstance) ;

另外,构造函数将— invocationList设置为null。所以,图8-2展示了委托创建之后,其重要的3个成员的值。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注