[转] 把委托说透(2):深入理解委托

上一篇随笔中我们通过示例逐步引入了委托,并比较了委托和接口。本文将重点剖析委托的实质。

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

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

委托揭秘

把委托说透(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 { get; set; }
 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 }

打开Reflector反编译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三个方法

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

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

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

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

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

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

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值分别如下图所示:

构造委托链

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

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

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

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

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

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

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

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

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

总结

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

在接下来的随笔中,我们将对.NET中委托的一个典型应用——事件,进行全面深入的介绍。

参考资料

CLR via C# 2nd Edition

时间: 2024-12-11 15:58:12

[转] 把委托说透(2):深入理解委托的相关文章

[转] 把委托说透(4):委托与设计模式

委托与很多设计模式都有着千丝万缕的联系,在前面的随笔中已经介绍了委托与策略模式的联系,本节主要来讨论委托与其他两个模式:观察者模式和模板方法模式. 委托与观察者模式 在.NET中,很多设计模式得到了广泛应用,如foreach关键字实现了迭代器模式.同样的,.NET中也内置了观察者模式的实现方式,这种方式就是委托. 观察者模式的一般实现 网上可以找到很多资料介绍观察者模式的实现,我这里介绍一种简单的退化后的观察者模式,即Subject类为具体类,在其之上不再进行抽象. 1 public class

理解委托类型

怕大家看了觉得乱七八糟不知道讲什么,先一句话描述下:如标题,在C#中委托是一种类型,如class一样也需要声明,定义,赋值,并与class在同一个层面 再介绍之前先看下相关内容的定义(我不是咬文嚼字,个人以为,有些东西的定义一定要没明白,不能含糊,不然只会是似懂非懂) delegate :(关键字->类型->引用类型[class,interface等等这些都是引用类型关键字]) delegate 是一种可用于封装命名或匿名方法的引用类型.(并不是所有的引用类型都是class) 是一种定义方法签

编写高质量代码改善C#程序的157个建议——建议44:理解委托中的协变

建议44:理解委托中的协变 委托中的泛型变量天然是部分支持协变的.为什么是“部分支持协变”?看下面示例: class Program { public delegate T GetEmployeeHanlder<T>(string name); static void Main() { GetEmployeeHanlder<Employee> getAEmployee = GetAManager; Employee e = getAEmployee("Mike"

进一步理解委托

前面一篇文章介绍了委托的基本知识,接下来就进一步研究一下委托. 委托类型 其实,刚开始觉得委托类型是一个比较难理解的概念,怎么也不觉得下面的"AssembleIphoneHandler"是一个类型. public delegate void AssembleIphoneHandler(); 按照正常的情况,如果我们要创建一个委托类型应该是: public class AssembleIphoneHandler : System.MulticastDelegate { } 但是,这种写法

理解委托(delegate)及为什么要使用委托

理解委托(delegate)及为什么要使用委托 委托:是一种定义方法签名的类型. 当实例化委托时,您可以将其实例与任何具有兼容签名的方法相关联. 您可以通过委托实例调用方法. 上述为官方说法,理解起来比较难,举个生活中的例子: 某人有三子,让他们各自带一样东西出门,并带回一头猎物.上面一句话可以理解为父亲对儿子的委托:猎物 办法(工具 某工具)-->delegate 猎物(返回值) 带回猎物(委托名)(工具(参数类型) x)-->delegate int GetValue(int i)三个人执

没有代码也让你理解委托的作用

假如我们现在有个机器人,它有一个功能或者叫作方法,叫做dosomething(),然后我们在里面写实现代码,例如快过年了,就写拜年的方法 ,OK,然后过年的时候它就会向大家拜年了,现在到这里还是很正常的,但是新年过去了之后呢?拜年这个方法也就不用使用了, 这个时候我们就需要机器人干其他事情了,不可能买一个机器人就只是为了拜年吧?我现在想要机器人扫地,所以我们就需要修改 dosomething这个方法了,那怎么修改了,我想应该是修改代码之后再植入到机器人的芯片吧!但是这样的操作方法是违反了开封 闭

C#知识体系(二)用案例来理解委托与事件

上一篇博客讲到了LinQ和lambda的常用方法 还有很多我们未知但c#设计团队已经为我们封装好的类和方法.随着我们不断的熟悉C#语言,渐渐的就会接触到其他的知识点,委托.事件.反射.线程.同步,异步.IO.套接字...这些东西我们平常用到的不多,都是些概念性的东西,也许是因为不熟悉而可以回避了使用这些东西,不可否认的是 就算不用这些我们依然能想到问题的解决办法.但是几乎所有语言都会有这些概念,因为在某些场景它们能发挥不可思议的能力. 其实我到现在还是没有掌握委托和事件,在工作或者设计中也尽量回

C#学习之初步理解委托、事件、匿名方法和Lambda

最经在学习LinqtoSql,然后扯到Lambda表达式,然后扯到匿名方法,然后扯到委托,最后扯到事件处理...后来发现对委托这个概念和事件处理这个过程理解得不是很清晰,遂得一下学习笔记.那里说得不对,请大家多多指教! 第一部分:理解委托 委托委托,顾名思义,就是类似于中间人的意思,有些事情你不做,委托别人去做,比如你想相亲,但你不想去主动约女孩子,那你可以交给媒婆去帮你约. 如果你学过C++,请把委托理解成函数指针,都是为了调用函数.函数指针可以调用符合该函数指针要求的函数.什么叫符合该函数指

使用委托中介租房理解委托与事件

委托和事件,.Net Framework中的应用非常广泛,然而,较好的理解委托和事件对很多接触C#时间不长的人来说并不容易.它们就像一道门槛儿,跨过去的,觉得太容易了,而没有过去的人每次见到委托事件就觉得心慌慌,浑身不自在. 我个人还是比较喜欢用面向对象的编程思想去理解逻辑程序,理解编程.下面就用委托中介公司租房子的示例理解使用委托流程: 1.定义委托和委托对象——租房人想要租房,但是没有房源,于是委托中介找房子:2.编写要委托的方法——租房中介有房源,可以找到房子:3.将要委托的方法传递给委托