深入理解C#委托的实质

开发 后端
本文重点剖析C#委托的实质。委托在本质上仍然是一个类,正如很多资料上所说的,委托是一种类型安全的函数回调机制。

本文是博客园麒麟.NET的《把委托说透》系列的第二篇,重点剖析C#委托的实质。

委托在本质上仍然是一个类,我们用delegate关键字声明的所有委托都继承自System.MulticastDelegate。后者又是继承自System.Delegate类,System.Delegate类则继承自System.Object。委托既然是一个类,那么它就可以被定义在任何地方,即可以定义在类的内部,也可以定义在类的外部。

正如很多资料上所说的,委托是一种类型安全的函数回调机制, 它不仅能够调用实例方法,也能调用静态方法,并且具备按顺序执行多个方法的能力。

C#委托揭秘

把委托说透(1)中可以看到,委托的使用其实是很简单的。尽管如此,其内部实现仍然相当复杂。.NET强大的编译器和CLR掩盖了这种复杂性。

为了解释方便,我们把(1)中的委托代码复制在下面,并做一处小小的改动,将LogToTextFile设置为实例方法。

  1. namespace DelegateSample  
  2. {  
  3.  
  4.     public delegate void Log(string message);  
  5.  
  6.     class UserService  
  7.     {  
  8.         public Log LogDelegate { getset; }  
  9.  
  10.         public UserService() { }  
  11.  
  12.         public void Register(User user)  
  13.         {  
  14.             if (user.Name == "Kirin")  
  15.             {  
  16.                 LogDelegate("注册失败,已经包含名为" + user.Name + "的用户");  
  17.             }  
  18.             else 
  19.             {  
  20.                 LogDelegate("注册成功!");  
  21.             }  
  22.         }  
  23.     }  
  24.  
  25.     class Program  
  26.     {  
  27.         static void Main(string[] args)  
  28.         {  
  29.             User user = new User { Name = "Kirin", Password = "123" };  
  30.             UserService service = new UserService();  
  31.             service.LogDelegate = LogToConsole;  
  32.             Program p = new Program();  
  33.             service.LogDelegate += p.LogToTextFile;  
  34.             service.Register(user);  
  35.  
  36.             Console.ReadLine();  
  37.         }  
  38.  
  39.         static void LogToConsole(string message)  
  40.         {  
  41.             Console.WriteLine(message);  
  42.         }  
  43.  
  44.         void LogToTextFile(string message)  
  45.         {  
  46.             using (StreamWriter sw = File.AppendText("log.txt"))  
  47.             {  
  48.                 sw.WriteLine(message);  
  49.                 sw.Flush();  
  50.                 sw.Close();  
  51.             }  
  52.         }  
  53.     }  
  54. }  
  55.  

打开Reflector反编译Log委托,可以看到Log类被编译为如下形式:

反编译Log委托

在上图中可以得出如下结论:

委托是一个类

可以很清晰的看出Log—>MulticastDelegate—>Delegate这种继承机制。

尽管委托继承自System.MulticastDelegate类,但我们并不能显示地声明一个继承自System.MulticastDelegate类的委托。委托必须使用delegate关键字声明,编译器会自动为我们生成继承代码。

由于委托继承自System.MulticastDelegate类,自然也继承MulticastDelegate类的字段、属性和方法。这些成员中,最重要的当属三个非公共字段,如下表所示:

字段名称 字段类型 描述
_target System.Object 该字段指明委托所调用的方法所在的实例类型。如果委托调用的为静态方法,该字段为null;如果为实例方法则为该方法所在的对象。
_methodPtr System.IntPtr 标识回调方法的指针。
_invocationList System.Object 在构建委托链时指向一个委托数组,在委托刚刚构建时通常为null。

由上表可以看出,每个委托对象实际上是对方法及其调用时操作的对象的封装。MulticastDelegate类还定义了两个只读公有实例属性:Target和Method,分别对应_target和_methodPtr。Target属性返回一个方法回调时操作的对象引用。如果是静态方法则返回null。Method属性返回一个标识回调方法的System.Reflection.MethodInfo对象。

编译器自动为委托创建了BeginInvoke、EndInvoke和Invoke三个方法

当我们在像调用普通的方法一样调用委托时,如

  1. LogDelegate("注册失败,已经包含名为" + user.Name + "的用户"); 

这时实际上调用的是编译器自动生成的Invoke方法

  1. LogDelegate.Invoke("注册失败,已经包含名为" + user.Name + "的用户"); 

使用IL DASM查看UserService的IL代码,可以验证以上结论,如下图所示:

使用IL DASM查看UserService的IL代码

在使用委托时,我们也可以显示调用Invoke方法(CLR 2.0)。

显示调用Invoke方法

Invoke方法的参数和返回值与委托是一致的。在调用Invoke方法时,会使用_target和_methodPtr字段。

BeginInvoke和EndInvoke方法用来实现异步调用,本文在此不进行讨论。

委托链

委托链是一个委托的集合,它允许我们调用这个集合中的委托所代表的所有方法(对于有返回值的方法,委托链的返回值为链表中最后一个方法的返回值,本文后面会有详细介绍)。在Delegate类中定义了3个静态方法来帮助我们操作委托链。

  1. public static Delegate Combine(params Delegate[] delegates);  
  2. public static Delegate Combine(Delegate a, Delegate b);  
  3. public static Delegate Remove(Delegate source, Delegate value);  

要理解委托链,我们首先基于前面的例子,重新声明两个委托:logDel1和logDel2。

  1. Log logDel1 = LogToConsole;  
  2. Program p = new Program();  
  3. Log logDel2 = p.LogToTextFile;  

这两个委托的_target、_methodPtr和_invocationList值分别如下图所示:

两个委托的_target、_methodPtr和_invocationList值

构造委托链

然后,我们使用Combin方法来构造一个委托链:

  1. Log logChain = null;  
  2. logChain = (Log)Delegate.Combine(logChain, logDel1);  

由于logChain初始为null,在使用Combin方法构造委托链时,将返回另外一个参数logDel1,再将logDel1的引用赋给logChain。这时logChain将指向logDel1所指向的对象。

logChain将指向logDel1所指向的对象

接下来我们将logDel2也添加到logChain中来:

  1. logChain = (Log)Delegate.Combine(logChain, logDel2); 

此时,由于logChain已经不再是null,将重新构建一个新的委托对象。该委托对象的_target和_methodPtr字段与logDel2(第二个参数)相同,_invocationList字段将指向一个委托数组。该委托数组中包含两个元素,第一个元素(索引为0)指向封装了LogToConsole方法的委托(即logDel1指向的委托);第二个元素(索引为1)指向封装了LogToTextFile方法的委托(即logDel2指向的委托)。最后,将这个新创建的委托对象的引用赋给logChain。

将这个新创建的委托对象的引用赋给logChain

若再将一个新的委托logDel3添加到委托链中,则仍然会构建一个新的委托对象,并将logDel3的引用添加到该委托对象_invocationList的末尾(此时链表共有3个元素)。然后,再将该委托对象的引用赋给logChain。而logChain之前指向的委托对象则等待垃圾回收。

至此,委托链构造完毕,我们来看看如何执行委托链表中的委托。由于logChain仍然指向一个委托对象,因此执行委托链表的语法与执行委托是一样的:

  1. logChain("执行委托链"); 

与普通的委托(如logDel1)所不同的是,logChain的_invocationList字段不为null。这时将首先遍历执行_invocationList中的所有委托。所执行的方法的顺序与添加的顺序一致,依次为LogToConsole、LogToTextFile。

委托Log的Invoke方法的实现用伪代码表示如下:

  1. public void Invoke(string message)  
  2. {   
  3.     Delegate[] delegateSet = _InvocationList as Delegate[];  
  4.     if (delegateSet != null)   
  5.     {  
  6.         // 如果委托数组不为空,则依次执行该委托数组中的委托  
  7.         foreach (Feedback d in delegateSet)  
  8.             d(value);  
  9.     }   
  10.     else   
  11.     {  
  12.         // 如果委托数组为空,则该委托不代表一个委托链  
  13.         // 按照正常方式执行该委托  
  14.         _methodPtr.Invoke(_target, value);  
  15.     }  
  16. }  

 包含返回值的委托的Invoke实现如下,假设返回值为string:

  1. public string Invoke(string message)  
  2. {  
  3.     string result = null;  
  4.     Delegate[] delegateSet = _InvocationList as Delegate[];  
  5.     if (delegateSet != null)  
  6.     {  
  7.         // 如果委托数组不为空,则依次执行该委托数组中的委托  
  8.         foreach (Feedback d in delegateSet)  
  9.             result = d(value);  
  10.     }  
  11.     else 
  12.     {  
  13.         // 如果委托数组为空,则该委托不代表一个委托链  
  14.         // 按照正常方式执行该委托  
  15.         result = _methodPtr.Invoke(_target, value);  
  16.     }  
  17.     return result;  
  18. }  

可以看到在委托链中,返回值为链表中最后一个委托的返回值。

那么如果对两个委托链调用Combine方法呢?

  1. Log logChain = null;  
  2. Log logChain1 = null;  
  3. Log logChain2 = null;  
  4. logChain1 = (Log)Delegate.Combine(logChain1, logDel1);  
  5. logChain1 = (Log)Delegate.Combine(logChain1, logDel2);  
  6. logChain2 = (Log)Delegate.Combine(logChain2, logDel3;  
  7. logChain2 = (Log)Delegate.Combine(logChain2, logDel4;  
  8. logChain = (Log)Delegate.Combine(logChain1, logChain2);  

最终的结果是,logChain的_target和_methodPtr均与logDel4相同(确切地说,两个委托对象的_methodPtr字段并不相同,但Method属性是相同的),而_invocationList中委托的顺序依次为logDel1、logDel2、logDel3、logDel4。

综上所述,可以对Delegate.Combine(Delegate A, Delegate B)方法做如下总结:

1. 如果A和B均为null,则返回null。

2. 如果A或B一个为null而另一个不为null,则返回不为null的委托。

3. 如果A和B均不为null,返回一个新的委托,该委托

    (1)_target字段与B的_target字段的值相同

    (2)Method属性与B的Method属性的值相同

    (3)_invocationList字段为一个委托数组,该数组中委托的顺序为:A中_invacationList所指向的委托数组 + B中_invacationList所指向的委托数组。

移除委托链

Combine方法用来向委托链中添加一个委托,而Remove方法用来从委托链中移除一个委托。

logChain = (Log)Delegate.Remove(logChain, new Log(LogToConsole));

当调用Remove时,会遍历(倒序)第一个参数(logChain)中的中的委托列表(_invocationList字段), 找到与第二个参数(new Log(LogToConsole))的_target和_methodPtr字段相匹配的委托,并将其从委托列表中移除。返回值需分以下几种情况,为了描述方便,我们将logChain记为A,将new Log(LogToConsole)记为B。

1. 如果A为null,返回null。

2. 如果B为null,返回A。

3. 如果A的_invocationList为null,即不包含委托链,那么如果A本身与B匹配,则返回null,否则返回A。

4. 如果A的_invocationList中不包含与B匹配的委托,则返回A。

5. 如果A的_invocationList中包含与B匹配的委托,则从链表中移除B,然后

    (1)如果A的链表中只剩下一个委托,则返回该委托。

    (2)如果A的链表中还剩下多个委托,将重新构建一个新的委托R(R的_invocationList字段为A的_invocationList移除了B之后的链表),并返回R。

注意,Remove方法只移除源委托的_invocationList列表中第一个匹配的委托,要想移除所有匹配的委托,可以使用RemoveAll方法。

有了委托链,在(1)中提出的第二个疑问就迎刃而解了。当用户希望使用多种日志记录方式的时候,使用委托链可以轻松地添加和删除某种日志记录方式,从而避免了人为地维护一个列表。

总结

本文首先介绍了C#委托的实质,委托是一个类,它继承自System.MulticastDelegate,而MulticastDelegate又继承自System.Delegate。然后重点剖析了委托链,讨论了如何创建和移除委托链。

【编辑推荐】

  1. C#委托实例简单分析
  2. 一个.NET委托的故事:彼得,老板和宇宙
  3. 解惑答疑:C#委托和事件
  4. 各版本.NET委托的写法回顾
  5. 换一个角度看.NET中的理解委托和事件
责任编辑:yangsai 来源: 博客园
相关推荐

2024-06-25 08:43:25

C#编程模型

2024-05-17 12:56:09

C#编程线程

2009-01-20 09:54:13

C# 3.0C#改进

2024-10-11 11:54:14

C#编写异步

2024-05-11 07:13:33

C#Task编程

2024-06-25 08:33:48

2009-08-20 18:11:08

C#异步委托

2024-04-10 12:14:36

C++指针算术运算

2024-07-15 08:21:26

TCPC#连接

2016-12-08 15:36:59

HashMap数据结构hash函数

2020-07-21 08:26:08

SpringSecurity过滤器

2010-06-01 15:25:27

JavaCLASSPATH

2022-05-06 16:18:00

Block和 C++OC 类lambda

2012-11-22 10:11:16

LispLisp教程

2019-06-25 10:32:19

UDP编程通信

2017-08-15 13:05:58

Serverless架构开发运维

2017-01-10 08:48:21

2024-02-21 21:14:20

编程语言开发Golang

2020-09-23 10:00:26

Redis数据库命令

2023-09-12 11:44:02

C++数据对齐
点赞
收藏

51CTO技术栈公众号