转载

重中之重:委托与事件

相关文章链接

编程之基础:数据类型(一)

编程之基础:数据类型(二)

高屋建瓴:梳理编程约定

动力之源:代码中的泵

难免的尴尬:代码依赖

可复用代码:组件的来龙去脉

物以类聚:对象也有生命

重中之重:委托与事件

  • 5.1 什么是.NET中的委托
    • 5.1.1 委托的结构
    • 5.1.2 委托链表
    • 5.1.3 委托的不可改变特性
    • 5.1.4 委托的作用
  • 5.2 事件与委托的关系
  • 5.3 使用事件编程
    • 5.3.1 注销跟注册事件同样重要
    • 5.3.2 多线程中使用事件
    • 5.3.3 委托链表的分步调用
    • 5.3.4 正确定义一个使用了事件的类
  • 5.4 弱委托
    • 5.4.1 强引用与弱引用
    • 5.4.2 弱委托定义
    • 5.4.3 弱委托使用场合
  • 5.5 本章回顾
  • 5.6 本章思考

委托是.NET编程中的重点之一,委托的作用简单概括起来就是"调用方法"。使用委托,我们可以异步(同步)调用方法、一次调用多个方法甚至可以将方法作为参数传递给别人供别人回调。程序的运行过程便是方法之间的调用过程,所以委托是.NET开发者必须掌握的知识点之一。.NET编程中的事件建立在委托的基础之上,要掌握事件的用法必须先了解委托。

5.1 什么是.NET中的委托

委托的字面意思为"把什么什么东西托付给某某人去做",偏向于一个动作,但是在.NET中,委托却是一个名词,表示"代理"或者"中间人"的意思。A本来要找B办事,但是它没有直接找B,而是托付给C,让C去找B把事儿给办了,如果按照"委托"字面意思去理解,"A找C"的这个行为叫"委托",但是在.NET中,C这个人叫"委托"。

重中之重:委托与事件

图5-1 .NET中委托含义

图5-1中A表示请求办事情的人(请求方),B是最终处理事情的人(应答方),C表示.NET中的委托。既然C是中间人,那么它肯定包含有B的一些信息,不然怎么去找B办事情?

注:本书中之后出现的所有与"委托"有关的词汇均指 .NET 中的委托,也就是图 5-1 中的 C 部分。另外,按照第二章中所讲的内容, A 可以称为 Client B 则称为 Server

5.1.1 委托的结构

委托的职责就是代替请求方去找应答方办事情,在程序中,体现为调用应答方的方法,换句话说,委托其实就是起到"调用方法"的作用。

程序中调用一个方法的必备条件是:知道要调用的方法,知道这个方法的所有者(如果该方法为实例方法)。因此一个委托中至少要包含图5-2中的信息:

重中之重:委托与事件

图5-2 委托组成

图5-2中显示一个委托的结构组成,它至少包含要调用的方法Method和方法的所有者Target(如果方法为静态方法,Target为null)。也就是说,委托是一种数据结构,我们可以把它看作是一种类型,类型里面包含一些成员。事实上,.NET中的委托就是一种类型,有着共同的基类Delegate,我们程序中定义的各种各样的委托都是从该类派生而来。

注:我们使用到的委托类型都派生自 MulticastDelegate 类,后者再派生自 Delegate 类型。系统不允许我们像定义普通类型的方式显式从这两个类型派生出新的委托,只能使用一种特殊定义类型的方法(后面有讲到)。另外,我们平时常说的"委托"是指一个委托类型的对象,本书中可以根据上下文判断"委托"是指委托类型还是委托类型的对象。

由于每个方法的签名不一样,因此一种委托只能负责调用一种类型的方法,也就是说,我们在定义委托类型的时候,必须提供它能够调用方法的签名,因此,.NET中规定,以如下形式去定义一个委托类型:

1 //Code 5-1 2 public delegate void DelegateName(object[] arg1);

像普通声明一个方法一样,提供方法名称、参数、访问修饰符以及返回值,然后在前面加上delegate关键字,这样就定义了一个委托类型,委托类型名称为DelegateName,它能够调用返回值为void,带有一个object[]类型参数的所有方法(包括实例方法和静态方法)。换句话说,就是所有符合该签名的方法都可以由DelegateName委托调用。注意我们不能显式在代码中这样去定义一个委托类型:

1 //Code 5-2 2 public class DelegateName:MulticastDelegate 3 { 4     // 5 }

编译器不允许以上代码Code 5-2通过编译。

注:"方法签名"指方法的参数个数、参数类型以及返回值等,具有相同签名的两个方法参数列表一致,返回值一致(名称可以不一样), int fun1(string a,int b) int fun2(string b,int a) 两个方法的签名相同。

委托类型定义完成后,怎么去实例化一个委托对象呢?其实很简单,跟实例化其它类型对象一样,我们可以通过new关键字,

 1 //Code 5-3  2 class Calculate  3 {  4     public Calculate()  5     {  6         //  7     }  8     public int DoDivision(int first,int second) //NO.1  9     { 10         return first/second; 11     } 12 } 13 private delegate int DivisionDelegate(int arg1,int arg2); //NO.2 14 class Program 15 { 16     static void Main() 17     { 18         Calculate c = new Calculate(); 19         DivisionDelegate d = new DivisionDelegate(c.DoDivision); //NO.3 20         int result = d(10,5); // int result = c.DoDivision(10,5); NO.4 21         Console.WriteLine("the result is " + result); 22     } 23 }

代码Code 5-3中我们定义了一个Calculate类型,专门负责除法运算(NO.1处),定义了一个DivisionDelegate委托(NO.2处)。在实际计算的时候,我们并没有直接调用Calculate类的DoDivision方法,而是先新建了一个委托对象d(NO.3处),给d的构造方法传递一个参数c.DoDivision。之后,我们通过这个委托d来计算10除以5的值(NO.4处)。整个过程中,我们没有直接使用对象c,而是通过委托d,这就像本节刚开始所说的:委托的职责就是代替请求方(Program类)去找应答方(c对象)办事情(除法运算)。代码中委托对象d的结构如下图5-3:

重中之重:委托与事件

图5-3 委托对象d内部结构

图5-3中显示,委托中的Target指向c对象,Method指向c对象的DoDivision方法,委托对象d就是对c.DoDivision(int,int)的一个封装。

另外,在我们使用new关键字创建委托实例时,会给它的构造方法传递了一个参数,该参数为一个方法名称。如果是实例方法,就应该使用"对象.方法名称"这样的格式(注意如果在同一个类中,对象默认为this,可以省略),如果是静态方法,就应该使用"类名称.方法名称"这样的格式(如果在同一个类中,类名称可以省略)。给构造方法传递的这个参数其实就是用来初始化委托内部的Target和Method两个成员。使用委托调用方法时,我们直接使用"委托对象(参数列表);"这样的格式即可,它等效于"委托对象.Invoke(参数列表)"。

注:给委托赋值的另外一种方式是:委托对象 = 方法。代码 Code 5-3 中赋值部分可以换成 DivisionDelegate d = c.DoDivision; ,含义跟用 new 关键字一样。另外,每一个自定义委托类型都包含一个 Invoke 方法,它的作用就是调用方法(与 BeginInvoke 方法对应,详见本书第六章),"委托对象 ( 参数列表 ) "只是调用方法的一种简写方式。

委托内部的Target为Object类型,表示方法的所有者,Method为MethodInfo类型,表示一个方法。通过委托调用方法"int result = d(10,5);",委托内部相当于:

1 //Code 5-4 2 int result = (int)Method.Invoke(Target,new Object[]{10,5});

意思就是在指定的对象(Target)上调用指定的方法(Method)。

5.1.2 委托链表

上一小节中提到的委托都是单委托,它只对一个方法进行封装,也就是说,使用单委托只能调用一个方法。

之前提到过,一个委托应该可以调用多个方法,只要这些方法的签名与该委托一致,那么怎样让一个委托同时调用两个或者两个以上的方法呢? 我们代码中很好实现,直接使用加法赋值运算符(+=)将多个方法附加到委托对象上,

 1 //Code 5-5  2 class Program  3 {  4     static void Fun1(object sender,EventArgs e)  5     {  6         //  7         Console.WriteLine("Call Fun1");  8     }  9     static void Fun2(object sender,EventArgs e) 10     { 11         // 12         Console.WriteLine("Call Fun2"); 13     } 14     static void Fun3(object sender,EventArgs e) 15     { 16         //... 17         Console.WriteLine("Call Fun3"); 18     } 19     static void Main() 20     { 21         EventHandler eh = new EventHandler(Fun1); //NO.1 22         eh += Fun2; //NO.2 23         eh += new EventHandler(Fun3); //NO.3 24         eh -= Fun2; //NO.4 25         eh(null,null); //NO.5 26         // print out: 27         // Call Fun1 28         // Call Fun3 29     } 30 }

代码Code 5-5中定义了一个EventHandler委托对象eh(NO.1处),按照先后顺序依次使用加法赋值运算符(+=)给它附加Fun2和Fun3方法(NO.2和NO.3处),然后使用减法赋值运算符(-=)移除Fun2方法(NO.4处),最后通过委托调用方法,依次输出"Call Fun1"和"Call Fun3"。由此可以得出三个结论:

(1)一个委托对象确实可以调用多个方法;

(2)这些方法可以按照附加顺序先后依次调用;

(3)可以从委托对象上移除一个方法,不影响其它方法。

注:确切的说,应该是将委托附加到委托对象上,另外代码中使用的都是静态方法,这时候委托内部 Target null += -= 运算符相当于 Delegate 类的静态方法 Delegate.Combine Delegate.Remove ,专门负责附加或移除委托操作。

根据以上三个结论,我们很有必要了解一下委托内部到底是怎样管理附加到它上面的方法,换句话说,委托内部到底有怎样的数据结构来组织和调用这些方法?

在学习数据结构中的"链表"时我们知道,每一个链表节点(Node)的结构都是相同的。链表表头、链表表尾以及中间的节点本质上是没有任何区别,我们可以将任意一个(或一串)节点附加到已有的一个(或一串)节点后面,从而形成一个更长的节点串。我们还能通过链表表头访问整个链表中的每一个节点(通过Next成员)。总之,只要知道了任意一个节点,我们就能访问该节点后面的所有节点(注意这里指的是单向链表)。单向链表结构类似如下图5-4:

重中之重:委托与事件

图5-4 单向链表结构

图5-4中实线矩形方框表示单向链表中的一个节点,所有节点都属于同一类型对象,因此结构相同。节点类Node代码类似如下:

重中之重:委托与事件
 1 //Code 5-6  2 class Node  3 {  4     private string _name; //node's name  5     private Node _next; // the next node  6     public string Name       7     {  8         get  9         { 10             return _name; 11         } 12         set 13         { 14             _name = value; 15         } 16     } 17     public Node Next 18     { 19         get 20         { 21             return _next; 22         } 23         set 24         { 25             _next = value; 26         } 27     } 28     public Node(string name) 29     { 30         _name = name; 31     } 32     public int GetNodesCount() //get the nodes' count from this to the end 33     { 34         int count = 0; 35         Node tmp = this; 36         do 37         { 38             count++; 39             tmp = tmp.Next; 40         } 41         while(tmp != null) 42         return count; 43     } 44     public Node[] GetNodesList() //get all nodes from this to the end 45     { 46         Node[] nodes = new Node[GetNodes()]; 47         int index = -1; 48         Node tmp = this; 49         do 50         { 51             index++; 52             nodes[index] = tmp; 53             tmp = tmp.Next; 54         } 55         while(tmp != null) 56         return nodes; 57     } 58     public void ShowMyInfo() //show node's info 59     { 60         Console.WriteLine("My name is " + _name); 61     } 62     public void ShowInfo() //show the all nodes' info from this to the end 63     { 64         ShowMyInfo(); 65         if(Next != null) 66         { 67             Next.ShowInfo(); 68         } 69     } 70 } 71 class Program 72 { 73     static void Main() 74     { 75         Node node1 = new Node("node1"); 76         Node node2 = new Node("node2"); 77         Node node3 = new Node("node3"); 78         node1.Next = node2; //NO.1 79         node2.Next = node3; //NO.2 80         Console.WriteLine("the count of the nodes from node1 to the end:" + node1.GetNodesCount()); //NO.3 81         Console.WriteLine("the count of the nodes from node2 to the end:" + node2.GetNodesCount()); 82         Console.WriteLine("the count of the nodes from node3 to the end:" + node3.GetNodesCount()); 83         Node[] nodes = node1.GetNodesList(); //NO.4 84         foreach(Node n in nodes) 85         { 86             n.ShowMyInfo(); //NO.5 87         } 88         node1.ShowInfo(); //NO.6 89         Console.Read(); 90     } 91 }
View Code

代码Code 5-6中我们可以通过一个节点访问该节点以及该节点所有的后续节点(NO3、NO.4、NO.5以及NO.6处),之所以能够这样,是因为每个节点中都保存有下一个节点的引用(Next引用)。代码中的node1. node2以及node3组成的单向链表在堆中的存储结构如下图5-5:

重中之重:委托与事件

图5-5 单向链表在堆中的结构

通过一个单向链表中的节点对象,我们能够访问附加到它后面的所有其它节点,委托对象也能够管理和访问附加到它上面的其它委托,也能管理一个"链表",那么,我们是否可以按照单向链表的结构去理解委托的内部结构呢?答案虽是肯定的,但是委托内部的"链表"结构跟单向链表的实现原理却不相同,它并不是通过Next引用与后续委托建立关联,而是将所有委托存放在一个数组中,类似如下图5-6:

注:准确来讲,委托内部结构不应该称为"链表"。

重中之重:委托与事件

图5-6 委托结构

图5-6中显示委托内部不仅仅有Target和Method成员,还有一个数组成员,用来存储附加到该委托对象中的其它委托。委托链在堆中的结构如下图5-7:

重中之重:委托与事件

图5-7 委托链表在堆中的结构

图5-7中显示delegate1中包含delegate2. delegate3以及delegate4的引用,注意delegate2. delegate3以及delegate4中的数组列表不可能再包含有其它的委托引用,也就是说包含关系最多只有两层,具体原因请参见下一小节有关委托的"不可改变"特性。

注:每一个委托类型都有一个公开的 GetInvocationList() 的方法,可以返回已附加到委托对象上的所有委托,也就是图 5-6 中数组列表。另外,我们平时不区分委托对象和委托链表,提到委托对象,它很有可能就表示一个委托链表,这跟单向链表只包含一个节点时道理类似。

既然现在委托可以调用多个方法,那么它的Invoke方法内部是怎样实现的呢?假如是一个简单的单委托,Invoke()方法内部直接调用Method.Invoke方法,但如果包含其它委托,那么它就需要遍历整个数组列表。代码类似如下(假设委托的签名为:返回值为null,含一个int类型参数):

 1 //Code 5-7  2 public void Invoke(int a)  3 {  4     Delegate[] ds = GetInvocationList(); //get all delegates in array  5     if(ds!=null) //contain a delegate chain  6     {  7         foreach(Delegate d in ds) // call each delegate  8         {  9             DelegateName dn = d as DelegateName; 10             dn(a); 11         } 12     } 13     else //don't contain a delegate chain 14     { 15         Method.Invoke(Target,new Object[]{a}); //call the Method on Target with argument 'a' 16     } 17 }

代码Code 5-7中委托的Invoke方法先判断该委托中是否包含其它委托,如果是,依次遍历列表调用这些委托;否则,说明当前委托是一个单委托,直接调用Method.Invoke()方法。

5.1.3 委托的 " 不可改变 " 特性

所谓"不可改变"(Immutable),就是指一个对象创建之后,它的内容不能再改变。比如常见的String类型,我们创建的一个String对象之后,之后在该对象上的所有操作都不会影响对象原来的值,

 1 //Code 5-8  2 Class Program  3 {  4     static void Main()  5     {  6         string a = "test"; // equal String a = new String("test");  7         a.ToUpper(); //NO.1  8         Console.WriteLine("a is " + a);  9         // print out: 10         // a is test 11     } 12 }

代码Code 5-8中a的值并没有因为调用了a.ToUpper()方法而改变,如果想要让a字符串都变为大写格式,必须使用"a = a.ToUpper();"这样的代码,a.ToUpper()方法会返回一个全新的String对象,a重新指向该新对象。注意这里的"不可改变"指的是对象实例,而不是对象引用,也就是说我们还是可以将a指向其它对象。如下图5-8:

重中之重:委托与事件

图5-8 String类型的不可变性

委托跟String类型一样,也是不可改变的。换句话说,一旦委托对象创建完成后,这个对象就不能再被更改,那么我们前面讲到的将一个委托附加到另外一个委托对象上形成一个委托链表又是怎么做到的呢?其实这个跟String.ToUpper()过程类似,我们对委托进行附加、移除等操作都会产生一个全新的委托,这些操作并不会改变原有委托对象。

 1 //Code 5-9  2 EventHandler eh = new EventHandler(Fun1); //NO.1  3 EventHandler tmp = eh; //tmp and eh point at the same delegate NO.2  4 EventHandler eh2 = new EventHandler(Fun2); //NO.3  5 eh += eh2; //NO.4  6 // equal eh = Delegate.Combine(eh, eh2) as EventHandler;  7 EventHandler tmp2 = eh; //tmp2 and eh point at the same delegate //NO.5  8 EventHandler eh3 = new EventHandler(Fun3); //NO.6  9 eh += eh3; //NO.7 10 //equal eh = Delegate.Combine(eh,eh3) as EventHandler;

上面代码Code 5-9最终会在堆中产生5个委托对象,NO.1处创建第一个,让eh指向它,NO.2处让tmp与eh指向同一个委托,NO.3处创建第二个,让eh2指向它,NO.4处合并了eh和eh2,但并没有改变原来的eh和eh2,而是新创建了第三个,并且让eh重新指向了新创建的第三个,NO.5处让tmp2与eh指向同一个委托,NO.6处创建第四个,让eh3指向它,NO.7处合并了eh和eh3,但并没有改变原来的eh和eh3,而是新创建了第五个,并且让eh重新指向了新创建的第五个。

我们对委托进行的每一个附加(+=或者Delegate.Combine)操作,都会创建一个全新的委托,该新创建委托的数组列表中包含原来两个委托数组列表内容的总和,这个过程并不会影响原来的委托,移除(-=或者Delegate.Remove)操作类似。附加或移除委托过程,见下图5-9:

重中之重:委托与事件

图5-9 附加或移除委托过程

图5-9中D1、D2、D3、D4、D5、D6以及c、d、e均为委托对象引用。Delegate.Combine(D1,D2)产生了D3,D1并没改变;Delegate.Combine(D3,D4)产生了D5,D5包含D3和D4中的数组列表内容之和,D3并没有改变;Delegate.Remove(D5,D1)产生了D6,D5并没有改变。由图5-9可以看出,委托包含关系最多只有两层,数组列表中的委托都属于单委托,单委托不再包含其它委托。

注:文中的委托对象、单委托、委托链表都是指一个委托类型的对象。

5.1.4 委托的作用

委托是一种数据结构,专门用来管理和组织方法,并负责调用这些方法。那么为什么需要委托来调用方法呢?原因有以下三点:

(1)编程中无时无刻都存在着"方法调用",委托可以更方便更有组织的管理我们需要调用的方法,理论上没有数量限制,只要是符合某一个委托签名的方法都可以由该委托管理。我们可以使用委托一次性(有先后顺序)地调用这些方法。在使用委托之前,我们调用方法是这样:

重中之重:委托与事件

图5-10 不使用委托调用方法

图5-10中为不使用委托直接调用方法的过程,我们每次只能调用一个方法。使用委托之后,我们可以调用一系列方法,如下图5-11:

重中之重:委托与事件

图5-11 使用委托调用方法

上图5-11为使用委托调用方法的过程,使用一个委托我们可以管理多个方法,并且一次性调用这些方法。能够统一管理和组织被调用的方法,在编程中起到一个非常重要的作用,如后面讲到的"事件编程"。

(2)使用普通方式调用方法只能是同步的(特殊方法除外),也就是说,被调用方法返回之前,调用线程一直处于等待状态。使用委托调用方法时,有两种方式可供选择,既可以同步调用也可以异步调用,前者和普通调用方式一样,而后者遵循"异步编程模型"的规律:方法的调用不会阻塞调用线程。

注:委托的异步调用关键在于它的 BeginInvoke 方法,该方法是 Invoke 方法的异步版本,详见第六章关于异步编程的介绍。

(3)有了委托,方法可以作为一种参数在代码中进行传递,这个类似于C++中的函数指针。委托的这种功能在框架中是非常有用的,框架一般由专业技术团队编写开发,由于框架的开发者并不知道框架使用者的具体代码,那么框架又是怎样调用使用者编写的代码呢?

框架有两种方式调用框架使用者编写的代码,一种便是面向抽象编程。框架中尽量不出现某个具体类型的引用,而是使用抽象化的基类引用或者接口引用代替。只要框架使用者编写的类型派生自抽象化的基类或实现了接口,框架均可以正确地调用它们。我们常见的使用using代码块来释放对象非托管资源就是一个例子:

1 //Code 5-10 2 using(FileStream fs = new FileStream(…)) 3 { 4     //use fs 5 }

代码Code 5-10中要求FileStream类必须实现了IDisposable接口(事实上确实如此)。代码Code 5-10经过编译后,与下面代码Code 5-11类似:

 1 //Code 5-11  2 IDisposable dispose_target = new FileStream(…);  3 try  4 {  5     //use filestream  6 }  7 finally  8 {  9     dispose_target.Dispose(); 10 }

如上代码Code 5-11所示,无论何时,FileStream对象都能正确地释放非托管资源。框架认为所有使用using来释放非托管资源的类型都已实现了IDisposable接口,因为只有这样,它才能够提前编写释放非托管资源的代码(如finally中的dispose_target.Dispose())。没有实现IDisposable接口的类型不能使用using关键字来释放非托管资源。

注:关于框架调用框架使用者代码的过程,可以参见第二章中关于对"协议"的介绍,如图 2-14

框架调用框架使用者代码的另外一种方式就是使用委托,将委托作为参数(变量)传递给框架,框架通过委托调用方法。异步编程中的一些方法往往带有委托类型的参数,比如FileStream.BeginRead、Socket.BeginReceive等等(后续章节有讲到)。这些方法都会带有一个AsyncCallBack委托类型的参数,我们在使用这些方法时,如果给它传递一个委托对象,当异步操作执行完毕后,框架自动会调用我们传递给它的委托。还有下一节中讲到的"事件",框架可以通过事件来调用框架使用者编写的代码,如事件发布者激发事件,调用事件注册者的事件处理程序。

注:我们使用 .NET 中预定义的一些类型、方法均可以当作框架中的一部分。

5.2 事件与委托的关系

委托的附加、移除以及调用,是没有范围限制的。如果一个类型包含一个委托成员,那么在类外部既可以给它附加或者移除委托,还可以调用这个委托。如下面代码:

 1 //Code 5-12  2 public delegate void DelegateName(int a,int b); //define a delegate type  3 class A  4 {  5     public DelegateName MyDelegate; //define a delegate member  6     Public A()  7     {  8         //  9     } 10     public void DoSomething() 11     { 12         // 13         if(MyDelegate != null) 14         { 15             //… if something happen or if something is OK 16             int arg1 = 1; int arg2 = 2; 17             MyDelegate(arg1,arg2); //then call the delegate 18         } 19     } 20     // 21 } 22 class Program 23 { 24     static void Fun1(int a,int b) 25     { 26         Console.WriteLine("the result is " + (a + b).ToString()); 27     } 28     static void Main() 29     { 30         A a = new A(); 31         a.MyDelegate += new DelegateName(Fun1); //NO.1 32         a.DoSomething(); //NO.2 33         a.MyDelegate(1,2); //NO.3 34     } 35 }

代码Code 5-12中,我们给a对象的MyDelegate附加一个方法后(NO.1处),a对象内部可以调用这个委托(NO.2处),a对象外部也可以调用这个委托(NO.3处)。也就是说,对MyDelegate委托成员的访问是没有限制的,从某种意义上讲,这违背了"面向对象"思想,因为类里面的有些功能不应该对外公开,比如这里的"委托调用",该操作应该只能发生在类型内部。如果我们把MyDelegate定义为private私有变量,那么我们在类外部就不能给它附加和移除方法,为了解决这个问题,.NET中提出了一种介于public和private之间的另外一种访问级别:在定义委托成员的时候给出event关键字进行修饰,前面加了event关键字修饰的public委托成员,只能在类外部进行附加和移除操作,而调用操作只能发生在类型内部。如果把代码Code 5-12中A类声明MyDelegate成员的代码改为:

1 //Code 5-13 2 public event DelegateName MyDelegate;

按照Code 5-13中的方式定义的委托只能在A类内部调用,之前代码Code 5-12中的NO.3处编译通不过。

我们把类中设置了event关键字的委托叫作"事件","事件"本质上就是委托对象。事件的出现,限制了委托调用只能发生在一个类型的内部,如下图5-12:

重中之重:委托与事件

图5-12 事件在程序调用中的位置

图5-12中server中的委托使用了event关键字修饰,只能在server内部调用,外部只能进行附加和移除方法操作。当符合某一条件时,server内部会调用委托,这个时间不由我们(Client)控制,而是由系统(Server)决定。因此大部分时候,事件在程序中起到了回调作用(关于调用与回调的区别,参见第二章)。

调用加了event关键字修饰的委托也称为"激发事件",调用方(图5-12中的server)称为"事件发布者",被调用方(图5-12中的client)称为"事件注册者"(或"事件观察者"、"事件订阅者"等,本书中统一称之为"事件注册者"),附加委托的过程称之为"注册事件"(或"绑定事件"、"监听事件"、"订阅事件"等,本书中统一称之为"注册事件"),移除委托的过程称之为"注销事件"。通过委托调用的方法称为"事件处理程序"。

注:将只能在类型内部调用的委托称之为"事件",主要是因为这些委托一般是当 server 中发生某件事件(或符合某个条件)时才被 server 调用。我们所熟知的 Button.Click TextBox.TextChanged Form.FormClosing 等事件,都属于这种情况。

"事件"在.NET中起到了重要作用,它为框架与框架使用者编写代码之间的交互做出了重大贡献。

5.3 使用事件编程

5.3.1 注销跟注册事件同样重要

前面在讲到委托结构组成的时候就知道,委托内部包含了要调用的方法(Method成员),以及该方法所属的对象(Target成员)。当我们注册事件时,其实就是附加委托的过程,将一个新委托附加到委托链表中。事件注册者向事件发布者注册事件后,发布者就会保存一个注册者的引用(委托中的Target成员),发布者激发事件,其实就是通过该引用调用注册者的事件处理程序。当我们注销事件时,其实就是移除发布者对注册者的引用。

第四章讲到,堆中的对象实例如果存在引用指向它,那么CLR就不会回收它在堆中占用的内存,哪怕这个对象已经没有使用价值。注册事件使一个新的引用指向了事件注册者,如果我们不及时注销事件,那么这个引用将会一直存在。

5.3.2 多线程中使用事件

在通常编程中,我们激发一个事件之前需要先判断该事件是否为空,如果不为空,我们就可以激发该事件(调用委托),类似代码如下:

1 //Code 5-14 2 public event MyDelegate SomeEvent; 3 if(SomeEvent != null) //NO.1 4 { 5     //do something 6     SomeEvent(arg1,arg2); //NO.2 call the delegate 7 }

代码Code 5-14中NO.1处先检查SomeEvent是否为空,如果为空,说明没有人注册过该事件,就不会执行if块中的语句;如果不为空,说明已经有人注册过该事件,就执行if块中的语句,调用委托(图中NO.2处)。在单线程中,上面代码没有任何问题,但是如果在多线程中,以上代码就有可能抛出异常:如果在NO.1处if判断为true,在NO.2执行之前,其它线程将SomeEvent改变为null,这时候再回头执行NO.2时,就会抛出NullReferenceException的异常。

注:本章前面讲到的"委托不可改变特性"指的是委托实例不可改变,类似 String 类型,委托引用仍然可以改变,所以 SomeEvent 可以指向其它实例,甚至指向 null

为了解决多线程中事件编程容易引发的异常,我们需要利用"委托不可改变"这一特点。由于我们对一个委托的任何操作都不会改变该委托本身,只会产生新的委托,那么我们完全可以在if判断语句之前,使用一个局部临时变量来指向委托实例,之后所有的操作都针对该局部临时变量。由于局部变量不可能被其它人修改,所以它永远都不会指向null。

1 //Code 5-15 2 MyDelegate tmp = SomeEvent; 3 if(tmp != null) //NO.1 4 { 5     //do something 6     tmp(arg1,arg2); //NO.2 7 }

上述代码Code 5-15中,先让tmp和SomeEvent指向同一委托实例,NO.1处if判断为true,if块中的tmp在任何时候都不会被其它线程修改为null,因为其它线程只能修改SomeEvent,并且我们对SomeEvent的任何操作都不会改变它所指向的委托实例。这种解决方法其实跟我们在做一个除法运算时检测除数是否为零的原理一样,如果在多线程中,我们检查完除数不为零后,直接进行除法运算,有可能抛出异常,如下代码:

 1 //Code 5-16  2 class A  3 {  4     //  5     public int x;  6     public A()  7     {  8         //  9     } 10     public int DoSomething(int y) 11     { 12         if(x != 0) //NO.1 13         { 14             return y/x; //NO.2 15         } 16         else 17         { 18             return 0; 19         } 20     } 21 }

上述代码Code 5-16中,如果NO.1处if判断为true后,在NO.2执行之前x的值被其它线程改变为0,那么代码执行到NO.2处时就会抛出异常。正确的做法是,使用一个临时变量存储x的值,之后所有的操作都是针对该临时变量。Code 5-16中类A的DoSomething方法可以修改为:

 1 //Code 5-17  2 public int DoSomething(int y)  3 {  4     int tmp = x;  5     if(tmp != 0) //NO.1  6     {  7         return y/tmp; //NO.2  8     }  9     else 10     { 11         return 0; 12     } 13 }

上述代码Code 5-17中,NO.1处if判断为true后,tmp的值就永远不会为零,其它线程对x的所有操作都不会影响到tmp的值,因此NO.2处不可能再有异常抛出。这个原理跟我们刚学习编程的时候碰到的形参和实参的关系一样,在值传递过程中,形参和实参是相互独立的,形参改变不会影响到实参。

注: .NET 中值类型赋值都是值传递,也就是说赋值后会产生一个一模一样的拷贝,两者之间是相互独立互不影响的。引用类型赋值也是值传递,因为它传递的是对象引用,赋值后两个引用指向堆中同一个实例,关于值类型与引用类型赋值请参见第三章。

5.3.3 委托链表的分步调用

调用任何方法都有可能出现异常,因此,通过委托调用方法时,我们最好把调用代码放在try/catch块中,类似如下:

 1 //Code 5-18  2 class A  3 {  4     //  5     public event MyDelegate SomeEvent;  6     public A()  7     {  8         //  9     } 10     public void DoSomething() 11     { 12         // 13         MyDelegate tmp = SomeEvent; //NO.1 14         if(tmp != null) 15         { 16             // 17             try //NO.2 18             { 19                 tmp(arg1,arg2); //NO.3 20             } 21             catch 22             { 23                  24             } 25         } 26     } 27 }

上述代码Code 5-18中,激发事件的代码(NO.3处)放在了try/catch块中,这样以来,万一事件注册者中的事件处理程序抛出了没有被处理的异常,try/catch便会捕获该异常,程序不会异常终止。

调用委托链时,如果某一个委托对应的方法抛出了异常,那么剩下的其它委托将不再调用。这个很容易理解,本来是按先后顺序依次调用方法,如果其中某一个抛出异常,剩下的肯定被跳过。为了解决这个问题,单单是将激发事件的代码放在try/catch块中是不够的,我们需要分步调用每个委托,将每一步的调用代码均放在try/catch块中。类A的DoSomething方法修改为:

 1 //Code 5-19  2 public void DoSomething()  3 {  4     //  5     MyDelegate tmp = SomeEvent; //NO.1  6     if(tmp != null)  7     {  8         //  9         Delegate[] delegates = tmp.GetInvocationList(); //NO.2 10         foreach(Delegate d in delegates) 11         { 12             MyDelegate del = d as MyDelegate; 13             try //NO.3 14             { 15                 del(arg1,arg2); //NO.4 16             } 17             catch 18             { 19                  20             } 21         } 22     } 23 }

上述代码Code 5-19中,我们没有直接使用tmp来调用委托链表,而是先通过tmp.GetInvocationList方法来获取委托链表中的委托集合(NO.2处),然后再使用foreach循环遍历集合,分步调用每个委托(NO.4处),分步调用过程均放在了try/catch块中,这样一来,任何一个方法抛出异常都不会影响到其它委托的调用。

注:在单线程中使用事件时,激发事件之前不需要使用一个临时委托变量,本小节所有代码为了与前一小节一致,都使用了临时委托。现实编程中,要看我们定义的类型是否在多线程环境中使用。 Winform 编程中的 Control 类(及其派生类)在设计之初就只让它们运行在 UI 线程中,因此它们激发事件时,都没有考虑多线程的情况。

5.3.4 正确定义一个使用了事件的类

前面说到过,.NET中的"事件"在框架与客户端代码交互过程中起到了关键作用。那么平常开发过程中,应该怎样去定义一个使用了事件的类型,既能够让该类型的使用者更容易地去使用它,也能够让该类型的开发者更方便地去维护它呢?其实定义一个使用了事件的类型有一套标准方法。下面从命名、激发事件以及组织事件三个方面详细说明:

(1)命名;

前面讲到过,通常情况下,当某件事情发生时,对象内部就会激发事件,通知事件注册者,调用对应的事件处理程序,因此代码中事件的命名最好跟这个发生的事情有关系。比如有一个负责收发Email的类,当接收到新的邮件时,应该会激发一个类似叫"NewEmailReceived"的事件,去通知注册了这个事件的其他人,我们最好不要将这个事件定义为"NewEmailReceive"。除了事件本身的命名,事件所属委托类型的命名也同样有标准格式,一般以"事件名+EventHandler"这种格式来给委托命名,前面提到的NewEmailReceived事件对应的委托类型名称应该是"NewEmailReceivedEventHandler"。激发事件时会传递一些参数,这些参数一般继承自EventArgs类型(后者为.NET框架预定义类型),以"事件名+EventArgs"来命名,比如前面提到的NewEmailReceived事件在激发时传递的参数类型名称应该是"NewEmailReceivedEventArgs"。下面为示例代码:

 1 //Code 5-20  2 private delegate void NewEmailReceivedEventHandler(object sender,NewEmailReceivedEventArgs e); //define a delegate NO.1  3 class EmailManager  4 {  5     //  6     public event NewEmailReceivedEventHandler NewEmailReceived; //define e event member NO.2  7     public EmailManager()  8     {  9         // 10     } 11 } 12 class NewEmailReceivedEventArgs:EventArgs //define event argument class derived from EventArgs NO.3 13 { 14     // 15     public NewEmailReceivedEventArgs() 16     { 17         // 18     } 19 }

上述代码Code 5-20中NO.1处定义一个委托,NO.2处使用该委托定义一个事件,NO.3处定义一个事件参数类,它派生自EventArgs类(通常情况下,EventArgs为所有事件参数类的基类,如果激发一个事件不带任何参数,那么可以直接使用EventArgs)。

注:事件的委托签名一般包含两个参数,一个 object 类型,表示事件发布者(自己),一个为从 EventArgs 派生出来的子类型,包含激发事件时所带的参数。

(2)激发事件;

当一个类内部发生某件事情(或者说某个条件成立时),类内部就会激发事件,通知事件的所有注册者。为了便于类型的使用者能够扩展这个类型,比如改变激发事件的逻辑,我们通常使用虚方法去激发事件,比如前面说到的邮件类EmailManager中激发NewEmailReceived事件应该是这样编写代码:

 1 //Code 5-21  2 private delegate void NewEmailReceivedEventHandler(object sender,NewEmailReceivedEventArgs e); //define a delegate NO.1  3 class EmailManager  4 {  5     //  6     public event NewEmailReceivedEventHandler NewEmailReceived; //define e event member NO.2  7     public EmailManager()  8     {  9         // 10     } 11     private void DoSomething() 12     { 13         // 14         if(/**/) //NO.4 15         { 16             NewEmailReceivedEventArgs e = new NewEmailReceivedEventArgs(); 17             OnNewEmailReceived(e); //NO.5 18         } 19     } 20     protected void virtual OnNewEmailReceived(NewEmailReceivedEventArgs e) //NO.6 21     { 22         if(NewEmailReceived != null) 23         { 24             NewEmailReceived(this,e); //NO.7 25         } 26     } 27 } 28 class NewEmailReceivedEventArgs:EventArgs //define event argument class derived from EventArgs NO.3 29 { 30     // 31     public NewEmailReceivedEventArgs() 32     { 33         // 34     } 35 }

上述代码Code 5-21中,NO.1、NO.2以及NO.3处含义与之前解释相同,NO.4处当类中某个条件成立时,并没有马上激发事件,而是调用了预先定义的一个虚方法OnNewEmailReceived(NO.6处),在该虚方法内部激发事件(NO.7处),之所以要把激发事件的代码放在一个单独的虚方法中,这是为了让从该类型(EmailManager)派生出来的子类能够重写虚方法,从而改变激发事件的逻辑。下面代码Code 5-22定义一个EmailManager的派生类EmailManagerEx:

 1 //Code 5-22  2 class EmailManagerEx:EmailManager  3 {  4     //  5     protected override void OnNewEmailReceived(NewEmailReceivedEventArgs e)  6     {  7         //…do something here  8         if(/**/) //NO.1  9         { 10             base.OnNewEmailReceived(e); //NO.2 11         } 12         else 13         { 14             // NO.3 15         } 16     } 17 }

如上代码Code 5-22所述,派生类中重写OnNewEmailReceived虚方法后,可以重新定义激发事件的逻辑。如果NO.1处if判断为true,则正常激发事件(NO.2处);否则,不激发事件(NO.3处)。我们能够在派生类EmailManagerEx的OnNewEmailReceived虚方法中做许许多多其它的事情,包括示例代码中"取消激发事件"。

虚方法的命名一般为"On+事件名",另外该虚方法必须定义为protected,因为派生类中很可能要调用基类的虚方法。

(3)组织事件。

事件类似属性,仅仅只是类型对外公开的一个中介,通过它可以访问类型内部的数据。换句话说,无论事件还是属性,真正存储数据的成员并没有对外公开,比如属性基本都对应有相应的私有字段,每个事件也对应有相应的私有委托成员。我们通过event关键字声明的公开事件,经过编译器编译之后,生成的代码类似如下:

 1 //Code 5-23  2 class EmailManager  3 {  4     //  5     private NewEmailReceivedEventHandler _newEmailReceived; //NO.1  6     public event NewEmailReceivedEventHandler NewEmailReceived  7     {  8         [MethodImpl(MethodImplOptions.Synchronized)] //NO.2  9         add //NO.3 10         { 11             _newEmailReceived = Delegate.Combine(_newEmailReceived,value) as NewEmailReceivedEventHandler; 12         } 13         [MethodImpl(MethodImplOptions.Synchronized)] 14         remove //NO.4 15         { 16             _newEmailReceived = Delegate.Remove(_newEmailReceived,value) as NewEmailReceivedEventHandler; 17         } 18     } 19 }

如上代码Code 5-23所示,编译器编译之后,将一个事件分成了两部分,一个私有委托变量_newEmailReceived(NO.1处)和一个事件访问器add/remove(NO3和NO.4处),前者类似一个字段,后者类似属性访问器set/get。可以看出,真正存储事件数据的是私有委托成员_newEmailReceived。

注:代码 Code 5-23 NO.2 [MethodImpl(MethodImplOptions.Synchronized)] 的作用类似 lock(this);, 为了解决多线程中访问同步问题,这个是官方给出的默认方法,该方法存在缺陷,因为使用 lock 加锁时,锁对象不应该是对外公开的, this 显然是对外公开的,很有可能出现对 this 重复加锁的情况,从而造成死锁。我们可以自己实现事件访问器 add/remove ,在其中添加自己的 lock 块,从而避免使用默认的 lock(this)

下图5-13为一个类中属性和事件的作用:

重中之重:委托与事件

图5-13 属性和事件的作用

有些类型包含的事件非常多,比如.NET3.5中System.Windows.Forms.Control就包含有69个公开事件。一个Control类(或其派生类)对象编译后,对象内部就会产生几十个类似代码Code 5-23中_newEmailReceived这样的私有委托成员,这无疑会增加内存消耗,为了解决这个问题,我们一般需要自己定义事件访问器add/remove,并且自己定义数据结构去存储组织事件数据,不再使用编译器默认生成的私有委托成员。微软在.NET中的标准做法是:定义一个类似Dictionary功能的容器类型EventHandlerList,专门用来存放委托。一个类型自定义事件访问器add/remove后的代码类似如下:

 1 //Code 5-24  2 class EmailManager  3 {  4     private static readonly object _newEmailReceived; //NO.1  5     private EventHandlerList _handlers = new EventHandlerList(); //NO.2  6     public event NewEmailReceivedEventHandler NewEmailReceived  7     {  8         add  9         { 10             _handlers.AddHandler(_newEmailReceived,value); //NO.3 11         } 12         remove 13         { 14             _handlers.RemoveHandler(_newEmailReceived,value); //NO.4 15         } 16     } 17     protected virtual void OnNewEmailReceived(NewEmailReceivedEventArgs e) 18     { 19         NewEmailReceivedEventHandler newEmailReceived = _handlers[_newEmailReceived] as NewEmailReceivedEventHandler; //NO.5 20         if(newEmailReceived != null) 21         { 22             newEmailReceived(this,e); 23         } 24     } 25 }

如上代码Code 5-24所述,自定义事件访问器add/remove后,使用EventHandlerList来存储事件数据,编译器不再生成默认的私有委托成员,所有的事件数据均存放在_handlers容器中(NO.2处),NO.1处定义了访问容器的key,NO.3以及NO.4处访问容器,NO.5处在激发事件之前,先判断容器_handlers中是否有人注册了该事件。

注:自己定义事件访问器还有其它很多作用,比如自己实现线程同步锁、给事件标注 [NonSerializable] 属性(编译器生成的私有委托成员默认都是 Serializable )等。

上面提到的命名规范、激发事件以及组织事件的方式,这三个是微软给出官方代码中的标准,所有官方源码资料中都遵守了这三个规范。我们平时开发过程中,也应该遵守这些原则,编写出更高质量的代码。

5.4 弱委托

5.4.1 强引用与弱引用

前面章节提到过,一个引用类型对象包括"引用"和"实例"两部分。如果堆中实例至少有一个引用指向它(不管该引用存在于栈中还是堆中),CLR就不能对其进行内存回收,同时我们一定能够通过引用访问到堆中实例。换句话说,引用与实例是一种"强关联"关系,我们称这种引用为"强引用"(Strong Reference),堆中对象实例能否被访问完全掌握在程序手中。

重中之重:委托与事件

图5-14 强引用

图5-14中a是A的强引用,b是B的强引用,B中又存在一个C的强引用,只要栈中a和b存在,堆中A、B以及C就会一直存在。我们平时编程过程中使用new关键字创建一个对象时返回的引用便是强引用,比如"A a=new A();"中,a就是强引用。

强引用的优点是程序中只要有强引用的存在,就一定能够访问到堆中的对象实例。由于只要有一个强引用存在,CLR就不会回收堆中的对象实例,这就会出现一个问题:如果我们程序中没有合理地管理好强引用,在该移除强引用的时候没有移除它们,这便会导致堆中的对象实例大量累积,时间一长,就会出现内存不足的情况,尤其当这些对象占用内存比较大的时候。管理好强引用并不是一件容易的事情,通常情况下,强引用在程序运行过程中不断的传递,到最后有些几乎发现不了它们的存在。虽然有时候开发者认为对象已经使用完毕,但是程序中还是会保存这些对象的强引用直到很长一段时间,甚至会一直到程序运行结束。在事件编程中,委托的Target成员,就是对事件注册者的强引用,如果事件注册者没有注销事件,这个Target强引用便会一直存在,堆中的事件注册者内存就一直不会被CLR回收,这对开发人员来讲,几乎是很难发觉的。

注:像" A a = new A(); "中的 a 称为"显式强引用( Explicit Strong Reference )",类似委托中包含的不明显的强引用,我们称之为"隐式强引用( Implicit Strong Reference )"。

对于"强引用",有一个概念与之对应,即"弱引用"。弱引用与对象实例之间属于一种"弱关联"关系,跟强引用与对象实例的关系不一样,就算程序中有弱引用指向堆中对象实例,CLR还是会把该对象实例当做回收目标。程序中使用弱引用访问对象实例之前必须先检查CLR有没有回收该对象内存。换句话说,当堆中一个对象实例只有弱引用指向它时,CLR可以回收它的内存。使用弱引用,堆中对象能否被访问同时掌握在程序和CLR手中。

重中之重:委托与事件

图5-15 弱引用

图5-15中a是A的弱引用,b是B的弱引用,B中又包含一个C的弱引用,不管a和b是否存在,堆中A、B以及C都有可能成为CLR的回收目标。

创建一个弱引用很简单,使用WeakReference类型,给它的构造方法传递一个强引用作为参数,代码如下:

 1 //Code 5-25  2 class A  3 {  4     public A()  5     {  6         //  7     }  8     public void DoSomething()  9     { 10         Console.WriteLine("I am OK"); 11     } 12     // 13 } 14 class Program 15 { 16     static void Main() 17     { 18         A a = new A(); 19         WeakReference wr = new WeakReference(a); //NO.1 20         a = null; //NO.2 21         //do something else 22         A tmp = wr.Target; //NO.3 23         if(wr.IsAlive) //NO.4 24         { 25             tmp.DoSomething(); //NO.5 26             tmp = null; 27         } 28         else 29         { 30             Console.WriteLine("A is dead"); 31         } 32     } 33 }

代码Code 5-25中创建了一个A对象的弱引用(NO.1处),然后马上将它的临时强引用a指向null(NO.2处),此时只有一个弱引用指向A对象。程序运行一段时间后(代码中do something处),当需要通过弱引用wr访问A对象的时候,我们必须先检查CLR有没有回收它的内存(NO.4处),如果没有,我们正常访问A对象;否则,我们不能再访问A对象。

在编程过程中,我们很难管理好强引用,从而造成不必要的内存开销。尤其前面讲到的"隐式强引用",在使用过程中不易发觉它们的存在。使用弱引用,CLR回收堆中对象内存不再根据程序中是否有弱引用指向它,因此程序中有没有多余的弱引用指向某个对象对CLR回收该对象内存没有任何影响。弱引用特别适合用于那些对程序依赖程度不高的对象,也就是那些对象生命期不是主要由程序控制的对象。比如事件编程中,事件发布者对事件注册者的存在与否不是很关心,如果注册者在,那就激发事件并通知注册者;如果注册者已经被CLR回收内存,那么就不通知它,这完全不会影响程序的运行。

5.4.2 弱委托定义

前面讲到过,委托包含两个部分:一个Object类型Target成员,代表被调用方法的所有者,如果方法为静态方法,Target为null;另一个是MethodInfo类型的Method成员,代表被调用方法。由于Target成员是一个强引用,所以只要委托存在,那么方法的所有者就会一直在堆中存在而不能被CLR回收。如果我们将委托中的Target强引用换成弱引用的话,那么不管委托存在与否,都不会影响方法的所有者在堆中内存的回收。这样一来,我们在使用委托调用方法之前需要先判断方法的所有者是否已经被CLR回收。我们称将Target成员换成弱引用之后的委托为"弱委托",弱委托定义如下:

 1 //Code 5-26  2 class WeakDelegate  3 {  4     WeakReference _weakRef; //NO.1  5     MethodInfo _method; //NO.2  6     public WeakDelegate(Delegate d)  7     {  8         _weakRef = new WeakReference(d.Target);  9         _methodInfo = d.Method; 10     } 11     public object Invoke(param object[] args) 12     { 13         object obj = _weakRef.Target; 14         if(_weakRef.IsAlive) //NO.3 15         { 16             return _method.Invoke(obj,args); //NO.4 17         } 18         else 19         { 20             return null; 21         } 22     } 23 }

如上代码Code 5-26所示,我们定义了一个WeakDelegate弱委托类型,它包含一个WeakReference类型_weakRef成员(NO.1处),它是一个弱引用,指向被调用方法的所有者,还包含一个MethodInfo类型_method成员(NO.2处),它表示委托要调用的方法。我们在弱委托的Invoke成员方法中,先判断被调用方法的所有者是否还在堆中(NO.3处),如果在,我们调用方法,否则返回null。

弱委托将委托与被调用方法的所有者之间的关系由"强关联"转换成了"弱关联",方法的所有者在堆中的生命期不再受委托的控制,下图5-16显示弱委托的结构:

重中之重:委托与事件

图5-16 弱委托结构

如上图5-16所示,图中上部分表示一个普通委托的结构,下部分表示一个弱委托的结构,虚线框表示弱引用,堆中实例的内存不再受该弱引用影响。

注:本小节示例代码中的 WeakDelegate 类型并没有提供类似 Delegate.Combine 以及 Delegate.Remove 这样操作委托链表的方法,当然也没有弱委托链表的功能,这些功能可以仿照单向链表的结构去实现,把每个弱委托都当作链表中的一个节点。请参照 5.1.2 小节中讲到的单向链表。

5.4.3 弱委托使用场合

我们在使用事件编程时,如果一个事件注册者向事件发布者注册了一个事件,那么发布者就会对注册者保存一个强引用。如果事件注册者未正确地注销事件,那么发布者的委托链表中就一直包含一个对该注册者的强引用,这样一来,注册者在堆中的内存永远都不会被CLR回收,如果这样的注册者属于大对象或者数目众多,很轻易就会造成堆中内存不足。弱委托就恰好能够解决这个问题,我们可以将事件编程中用到的委托替换为弱委托,那么事件发布者与事件注册者的关系如下图5-17:

重中之重:委托与事件

图5-17 弱委托在事件编程中的应用

如上图5-17所示,事件发布者中不再保留对事件注册者的强引用。当发布者激发事件时,先判断注册者是否存在(堆中内存是否被CLR回收),如果存在,就通知注册者;否则将对应弱委托从链表中删除。

注:弱委托链表请读者自己去实现。

5.5 本章回顾

委托与事件几乎出现在.NET编程的每一个地方,它们是.NET中最重要的知识点之一。程序的运行就是一个个调用与被调用的过程,而委托的主要作用就是"调用方法",它是衔接调用者与被调用者的桥梁。本章开头介绍了.NET中委托的概念和组成结构,同时介绍了委托链表以及它的"不可改变"特性;之后介绍了委托与事件的关系,我们明白了事件是一种特殊的委托对象;紧接着讲到了.NET中使用事件编程时需要关注的几条注意事项,它们是在事件编程过程中常遇到的陷阱;章节最后还提到了"弱引用"和"弱委托"的概念以及它们的实现原理,"弱委托"是解决内存泄露的一种有效方法。

本章提到了委托的三个作用:第一,它允许把方法作为参数,传递给其它的模块;第二,它允许我们同时调用多个具有相同签名的方法;第三,它允许我们异步调用任何方法。这三个作用奠定了委托在.NET编程中的绝对重要地位。

5.6 本章思考

1.简述委托包含哪两个重要部分。

A:委托包含两个重要组成:Method和Target,分别代表委托要调用的方法和该方法所属的对象(如果为静态方法,则Target为null)。

2.怎样简单地说明委托的不可改变特性?

A:对委托的所有操作,均需要将操作后的结果在进行赋值,比如使用"+="、"-="将操作后的结果赋值给原委托变量。这说明对委托的操作均不能改变委托本身。

3."事件是委托对象"是否准确?

A:准确。.NET中的事件是一种特殊的委托对象,即在定义委托对象时,在声明语句前增加了"event"关键字。事件的出现确保委托的调用只能发生在类型内部。

4.为什么说委托是.NET中的"重中之重"?

A:因为程序的运行过程就是方法的不断调用过程,而委托的作用就是"调用方法",它不仅能够将方法作为参数传递,还能同时(同步或异步)调用多个具有相同签名的方法。

5.弱委托的关键是什么?

A:弱委托的关键是弱引用,弱委托是通过弱引用实现的。

正文到此结束
Loading...