.net事件和委托的解析
2013-01-29 10:58
441 查看
.net中事件最常用在“观察者”设计模式中,事件的发布者(subject)定义一个事件,事件的观察者(observer)注册这个事件,当发布者激发该事件时,所有的观察者就会响应该事件(表现为调用各自的事件处理程序)。知道这个逻辑过程后,我们可以写出以下代码:
以上就是一个最最原始的含有事件类的定义。外部对象可以注册Subject对象的XX事件,当某一条件满足时,Subject对象就会激发XX事件,所以观察者作出响应。
注:编码中请按照标准的命名方式,事件名、事件参数名、虚方法名、参数名等等,标准请参考微软。
事件观察者注册事件代码为:
以上是一个最简单的“事件编程”结构代码,其余所有的写法都是从以上扩展出来的,基本原理不变。
升级:
在定义事件变量时,有时候我们可以这样写:
其余代码跟之前一样,升级后的代码显示的实现了“add/remove”,显示实现“add/remove”的好处网上很多人都说可以在注册事件之前添加额外的逻辑,这个就像“属性”和“字段”的关系,
没错,确实与“属性(Property)”的作用差不多,但它不止这一个好处,我们知道(不知道的上网看看),在多线程编程中,很重要的一点就是要保证对象“线程安全”,因为多线程同时访问同一资源时,会出现预想不到的结果。当然,在“事件编程”中也要考虑多线程的情况。“引子”部分代码经过编译器编译后,确实可以解决多线程问题,但是存在问题,它经过编译后:
以上转换为编译器自动完成,事件(取消)注册(+=、-=)间接转换由add_XX和remove_XX代劳,通过在add_XX方法和remove_XX方法前面添加类似[MethodImpl(MethodImplOptions.Synchronized)]声明,表明该方法为同步方法,也就是说多线程访问同一Subject对象时,同时只能有一个线程访问add_XX或者是remove_XX,这就确保了不可能同时存在两个线程操作_xx这个委托链表,也就不可能发生不可预测结果。那么,[MethodImpl(MethodImplOptions.Synchronized)]是怎么做到线程同步的呢?其实查看IL语言,我们不难发现,[MethodImpl(MethodImplOptions.Synchronized)]的作用类似于下:
如我们所见,它就相当于给自己加了一个同步锁,lock(this),我不知道诸位在使用同步锁的时候有没有刻意去避免lock(this)这种,我要说的是,使用这种同步锁要谨慎。原因至少两个:
1) 将自己(Subject对象)作为锁定目标的话,客户端代码中很可能仍以自己为目标使用同步锁,造成死锁现象。因为this是暴露给所有人的,包括代码使用者。
2) 当Subject类包含多个事件,XX1、XX2、XX3、XX4…时,每注册(或取消)一个事件时,都需要锁定同一目标(Subject对象),这完全没必要。因为不同的事件有不同的委托链表,多个线程完全可以同时访问不同的委托链表。然而,编译器还是这样做了。
在一个线程中执行sub.XX1+=new XXEventHandler(…)(间接调用sub.add_XX1(new XXEventHandler(…)))的时候,完全可以在另一线程中同时执行 sub.XX2+=new EventHandler(…)(间接调用sub.add_XX2(new EventHandler(…)))。_xx1和_xx2两个没有任何联系,访问他们更不需要线程同步。如果这样做了,影响性能效率(编译器自动转换成的代码就是这样子)。
结合以上两点,可以将“升级”部分代码修改为以下,从而可以很好的解决“线程安全”问题而且不会像编译器自动转换的代码那样影响效率:
在Subject类中增加一个同步锁目标“_xxSync”,不再以对象本身为同步锁目标,这样_xxSync只在类内部可见(客户端代码不可使用该对象作为同步锁目标),不会出现死锁现象。另外,如果Subject有多个事件,那么我们可以完全增加多个类似“_xxSync”这样的东西,比如“_xx1Sync、_xx2Sync…”等等,每个同步锁目标之间没有任何关联。
当一个类(比如前面提到的Subject)中包含的事件增多时,几十个甚至几百个,而且派生类还会增加事件,在这种情况下,我们需要统一管理这些事件,由一个集合来统一管理这些事件是个不错的选择,比如:
存放事件委托链表的容器为Dictionary<object,Delegate>类型,该容器存放各个委托链表的表头,每当有一个“事件注册”的动作发生时,先查找字典中是否有表头,如果有,直接加到表头后面;如果没有,向字典中新加一个表头。“事件注销”操作类似。
图1
字典的作用是将每个委托链表的表头组织起来,便于查询访问。可能有人已经看出来修改后的代码并没有考虑“线程安全”问题,的确,引进了集合去管理委托链表之后,再也没办法解决“线程安全”而又不影响效率了,因为现在各个事件不再是独立存在的,它们都放在了同一集合。另外,集合Dictionary<object,Delegate>声明为protected,子类完全可以使用该集合对子类的事件委托链表进行管理。
注:上图中委托链中各节点引用的都是实例方法,没有列举静态方法。
其实,.net中所有从System.Windows.Forms.Control类继承下来的类,都是用这种方式去维护事件委托链表的,只不过它不是用的字典(我只是用字典模拟),它使用一个EventHandlerList类对象来存储所有的委托链表表头,作用跟Dictionary<object,Delegate>差不多,并且,.net中也没去处理“线程安全”问题。总之,CLR在处理“线程安全”问题做得不是足够好,当然,一般事件编程也基本用在单线程中(比如Winform中的UI线程中),打个比方,在UI线程中创建的Control(或其派生类),基本上都在同一线程中访问它,基本不涉及跨线程去访问Control(或其派生类),所以大可不必担心事件编程中遇到“线程安全”问题。
事件编程中的内存泄露
说到“内存泄露”,可能很多人认为这不应该是.net讨论的问题,因为GC自动回收内存,不需要编程的人去管理内存,其实不然。凡是发生了不能及时释放内存的情况,都可以叫“内存泄露”,.net中包括“托管内存”也包括“非托管内存”,前者由GC管理,后者必然由编程者考虑了(类似C++中的内存),这里我们讨论的是前者,也就是托管内存的泄露。
我们知道(假设诸位都知道),当一个托管堆中的对象不可达时,也就是程序中没有对该对象有引用时,该对象所占堆内存就属于GC回收的范围了。可是,如果编程者认为一个对象生命期应该结束(该对象不再使用)的时候,同时也理所当然地认为GC会回收该对象在堆中占用的内存时,情况往往不是TA所认为的那样,应为很有可能(概率很大),该对象在其他的地方仍然被引用,而且该引用相对来说不会很明显,我们叫这个为“隐式强引用”(Implicit
strong reference),而对于Class A = new Class();这样的代码,A就是“显示强引用”(Explicit strong reference)了。(至于什么是强引用什么是弱引用,这个在这里我就不说了)那么,不管是“显示强引用”还是“隐式强引用”都属于“强引用”,一个对象有一个强引用存在的话,GC就不会对它进行内存回收。
事件编程中,经常会产生“隐式强引用”,参考前面的“图1”中委托链表中的每个节点都包含一个target,当一个事件观察者向发布者注册一个事件时,那么,发布者就会保持一个观察者的强引用,这个强引用不是很明显,因此我们称之为隐式强引用。因此,当观察者被编程者理所当然地认为生命期结束了,再没有任何对它的引用存在时,事件发布者却依然保持了一个强引用。如下图:
图2
尽管有时候,Observer生命期结束(我们理所当然地那样认为),Subject(发布者)却依旧对Observer有一个强引用(strong reference)(图2中红色箭头),该引用称作为“隐式强引用”。GC不会对Observer进行内存回收,因为还有强引用存在。如果Observer为大对象,且系统存在很多这样的Observer,当系统运行时间足够长,托管堆中的“僵尸对象”(有些对象虽然已经没有使用价值了,但是程序中依旧存在对它的强引用)越来越多,总有一个时刻,内存不足,程序崩溃。
事件编程中引起的异常
其实还是因为我们的Observer注册了事件,但在Observer生命期结束(编程者认为的)时,释放了一些必备资源,但是Subject还是对Observer有一个强引用,当事件发生后,Subject还是会通知Observer,如果Observer在处理事件的时候,也就是事件处理程序中用到了之前已经释放了的“必备资源”,程序就会出错。导致这个异常的原因就是,编程者以为对象已经死了,将其资源释放,但对象本质上还未死去,仍然会处理它注册过的事件。
form1为Observer,form2为Subject,form1监听form2的Click事件,在事件处理程序中将自己Show出来,一切运行良好,但是,当form1关闭后,再次点击form2激发Click事件时,程序报错,提示form1已经disposed。原因就是我们关闭form1时,认为form1生命期已经结束了,事实上并非如此,form2中还有对form1的引用,当事件发生后,还是会通知form1,调用form1的事件处理程序(form2_Click),而碰巧的是,事件处理程序中调用了this.Show()方法,意思要将form1显示出来,可此时form1已经关闭了。
小结
不管是内存泄露还是引起的异常,都是因为我们注册了某些事件,在对象生命期结束时,没有及时将已注册的事件注销,告诉事件发布者“我已死,请将我的引用删除”。因此一个简单的方法就是在对象生命期结束时将所有的事件注销,但这个只对简单的代码结构有效,复杂的系统几乎无效,事件太多,根本无法记录已注册的事件,再者,你有时候根本不知道对象什么时候生命期结束。下次介绍利用弱引用概念(Weak reference)引申出来的弱委托(Weak delegate),它能有效地解决事件编程中内存泄露问题。原理就是将图2中每个节点中的Target由原来的强引用(Strong
Reference)改为弱引用(Weak Reference)。
MSDN上对Delegate(委托)的解释:
表示委托,委托是一种数据结构,它引用静态方法或引用类实例及该类的实例方法。
我们先不去管网上对“委托”的其他形象比如,比如“类似函数指针”、“对同一类方法的签名”等等。先来看看MSDN上的解释是个什么意思。首先它是一种数据结构,基本可以看做包含两个东西,一个是方法,一个是方法的所有者,即对象(静态方法的所有者为类)。那么我们可以简单的用图来画一下:
图1
如果对委托比较熟悉的人可能已经知道,所有我们定义的委托变量,都有两个属性Target和Method,这也正验证了上图的正确性。
那么Event(事件)是个什么东西?看一下MSDN对“事件”的一段解释:
在发生其他类或对象关注的事情时,类或对象可通过事件通知它们。发送(或引发)事件的类称为“发行者”,接收(或处理)事件的类称为“订户”。
这个解释基本上没什么用,因为它跟之前谈到的“委托”没任何联系,那我们再回忆一下我们之前在代码中是怎么去定义一个事件的?书上说网上也说,定义一个委托变量,在声明前面加一个event关键字,event关键字小写,那么这个委托变量就可以叫事件,编写代码时,VS IDE智能提示会在事件前面有一个“闪电”的图标。那么,根据这个描述,事件明显就是一种特殊的委托实例,没错,事件就是一种特殊的委托实例,它具备所有委托所具备的特点(有一些限制,跟主题没关系,略过)。
当事件的观察者(Observer)向事件的发布者(Subject)注册一个事件时,事件发布者就会产生一个对事件观察者的强引用(strong reference)(不然,当事件发生时,怎么才能通知观察者),也就是说,“注册事件”这个动作将事件的发布者和事件的观察者绑定到了一起,具体体现在:发布者包含了一个对观察者的强引用。
结合上一篇文章中的“图2”,我们可以看出,一个事件对应一个委托链,激发事件的过程可以看做:遍历这个委托链,在每一个节点中的Target上调用Method。这就完成了“事件发布者->激发事件->通知观察者->观察者响应事件->调用事件处理程序”这一过程。那么,对于一个事件,每被注册一次,它对应的委托链上就应该增加一个节点,该节点中存储着观察者的信息,由于同一个事件,同一个对象可能注册好几次,因此某一事件对应委托链中的节点的个数并不等于该事件的观察者数目,一般前者大于后者。再来看一张图:
图2
由此看出,委托链维持着事件发布者与事件观察者之间的联系,在某些情况下,当我们认为我们程序中的观察者已死(代码中再没有对观察者的引用),GC会回收观察者的内存时,实际情况往往不是我们认为的这样,由于委托链中仍然有对观察者的强引用,GC是不会对它进行内存回收的,这样一来,如果观察者数目大,观察者属于大对象,系统运行时间过长,就会导致内存不足。
注:GC回收对象内存的前提是程序中不再有该对象的强引用(strong reference)。
解决以上问题关键在于,怎么在观察者即将死亡的时候通知发布者将自己的引用删除,但是这个几乎做不到,因为你根本不知道观察者什么时候死亡,再者,当系统复杂庞大之后,你根本不知道谁是谁的观察者,谁又暗自拥有谁的强引用。所以,最好的情况是,观察者在必要的时候,能够正常的被GC回收内存,对于事件发布者来说,观察者的存在与否没有多大关系,如果观察者还存在,那么当事件发生时就通知观察者,如果观察者不存在了,那么当事件发生时就不通知观察者。这就要求事件发布者对于观察者之间的关联不能像之前那种“强引用”的关系,而应该是“弱引用”的关系。
因此,我们可以仿照原有的Delegate数据结构,模拟一种新的数据结构,将原来Target这个“强引用”变为“弱引用”,我们取名叫做“弱委托”(WeakDelegate),代码类似如下:
包含两个属性WeakTarget和Method,分别对应原来委托的Target和Method属性,一个Invoke公共方法,对应原来委托的Invoke方法,用于执行委托。
由此一来,图2中委托链中的各个节点就可以由原来的Delegate(其派生类,我们自己定义的委托)变为我们现在的WeakDelegate,那么在写事件发布者类(Subject)的代码时,我们需要自己来维持这些委托链了,如下:
在事件发布者中,我定义了三个使用我们自己WeakDelegate的事件,由_delegateList管理,分别是XX1、XX2、XXn事件,他们对应的委托链中的每一个节点属于WeakDelegate类型,不会保存观察者的强引用。另外为了对比,我定义了一个传统的使用Delegate的事件CommonEvent,它对应的委托链中的每一个节点属于Delegate(派生类)类型,保存观察者的强引用。
定义了一个观察者(Observer)类,代码为:
该类除了注册Subject类的事件外,没有其他内容,当事件发生后,输出与该事件相关的信息。
测试代码如下:
被注释部分事件发布者与观察者之间属于弱关联,观察者能够正常死亡;未被注释部分事件发布者与观察者属于强关联,观察者不能正常死亡。
运行结果如下:
图3
图4
1 Class Subject 2 { 3 public event XXEventHandler XX; 4 protected virtual void OnXX(XXEventArgs e) 5 { 6 If(XX!=null) 7 { 8 XX(this,e); 9 } 10 } 11 public void DoSomething() 12 { 13 //符合某一条件 14 OnXX(new XXEventArgs()); 15 } 16 } 17 delegate void XXEventHandler(object sender,XXEventArgs e); 18 Class XXEventArgs:EventArgs 19 { 20 21 }
以上就是一个最最原始的含有事件类的定义。外部对象可以注册Subject对象的XX事件,当某一条件满足时,Subject对象就会激发XX事件,所以观察者作出响应。
注:编码中请按照标准的命名方式,事件名、事件参数名、虚方法名、参数名等等,标准请参考微软。
事件观察者注册事件代码为:
Subject sub = new Subject(); Sub.XX += new XXEventHandler(sub_XX); void sub_XX(object sender,XXEventArgs e) { //do something }
以上是一个最简单的“事件编程”结构代码,其余所有的写法都是从以上扩展出来的,基本原理不变。
升级:
在定义事件变量时,有时候我们可以这样写:
1 Class Subject 2 { 3 private XXEventHandler _xx; 4 public event XXEventHandler XX 5 { 6 add 7 { 8 _xx = (XXEventHandler)Delegate.Combine(_xx,value); 9 } 10 remove 11 { 12 _xx = (XXEventHandler)Delegate.Remove(_xx,value); 13 } 14 } 15 protected virtual void OnXX(XXEventArgs e) 16 { 17 if(_xx!=null) 18 { 19 _xx(this,e); 20 } 21 } 22 public void DoSomething() 23 { 24 //符合某一条件 25 OnXX(new XXEventArgs()); 26 } 27
其余代码跟之前一样,升级后的代码显示的实现了“add/remove”,显示实现“add/remove”的好处网上很多人都说可以在注册事件之前添加额外的逻辑,这个就像“属性”和“字段”的关系,
1 public event XXEventHandler XX 2 { 3 add 4 { 5 //添加逻辑 6 _xx = (XXEventHandler)Delegate.Combine(_xx,value); 7 } 8 remove 9 { 10 //添加逻辑 11 _xx = (XXEventHandler)Delegate.Remove(_xx,value); 12 } 13 }
没错,确实与“属性(Property)”的作用差不多,但它不止这一个好处,我们知道(不知道的上网看看),在多线程编程中,很重要的一点就是要保证对象“线程安全”,因为多线程同时访问同一资源时,会出现预想不到的结果。当然,在“事件编程”中也要考虑多线程的情况。“引子”部分代码经过编译器编译后,确实可以解决多线程问题,但是存在问题,它经过编译后:
1 public event XXEventHandler XX; 2 //该行代码编译后类似如下: 3 4 private XXEventHandler _xx; 5 [MethodImpl(MethodImplOptions.Synchronized)] 6 public void add_XX(XXEventHandler handler) 7 { 8 _xx = (XXEventHandler)Delegate.Combine(_xx,handler); 9 } 10 11 [MethodImpl(MethodImplOptions.Synchronized)] 12 public void remove_XX(XXEventHandler handler) 13 { 14 _xx = (XXEventHandler)Delegate.Remove(_xx,handler); 15 }
以上转换为编译器自动完成,事件(取消)注册(+=、-=)间接转换由add_XX和remove_XX代劳,通过在add_XX方法和remove_XX方法前面添加类似[MethodImpl(MethodImplOptions.Synchronized)]声明,表明该方法为同步方法,也就是说多线程访问同一Subject对象时,同时只能有一个线程访问add_XX或者是remove_XX,这就确保了不可能同时存在两个线程操作_xx这个委托链表,也就不可能发生不可预测结果。那么,[MethodImpl(MethodImplOptions.Synchronized)]是怎么做到线程同步的呢?其实查看IL语言,我们不难发现,[MethodImpl(MethodImplOptions.Synchronized)]的作用类似于下:
1 Class Subject 2 { 3 private XXEventHandler _xx; 4 public void add_XX(XXEventHandler handler) 5 { 6 lock(this) 7 { 8 _xx = (XXEventHandler)Delegate.Combine(_xx,handler); 9 } 10 } 11 public void remove_XX(XXEventHandler handler) 12 { 13 lock(this) 14 { 15 _xx = (XXEventHandler)Delegate.Remove(_xx,handler); 16 } 17 } 18 }
如我们所见,它就相当于给自己加了一个同步锁,lock(this),我不知道诸位在使用同步锁的时候有没有刻意去避免lock(this)这种,我要说的是,使用这种同步锁要谨慎。原因至少两个:
1) 将自己(Subject对象)作为锁定目标的话,客户端代码中很可能仍以自己为目标使用同步锁,造成死锁现象。因为this是暴露给所有人的,包括代码使用者。
1 private void DoWork(Subject sub) //客户端代码 2 { 3 lock(sub) //客户端代码锁定sub对象 4 { 5 sub.XX+=new XXEventHandler(…); //嵌套锁定同一目标 6 // sub.add_XX(new XXEventHandler(…));相当于调用add_XX,出现死锁 7 // 8 // 9 // 10 //do other thing 11 } 12 }
2) 当Subject类包含多个事件,XX1、XX2、XX3、XX4…时,每注册(或取消)一个事件时,都需要锁定同一目标(Subject对象),这完全没必要。因为不同的事件有不同的委托链表,多个线程完全可以同时访问不同的委托链表。然而,编译器还是这样做了。
1 Class Subject 2 { 3 private XXEventHandler _xx1 4 private EventHandler _xx2; 5 public void add_XX1(XXEventHandler handler) 6 { 7 lock(this) 8 { 9 _xx1 = (XXEventHandler)Delegate.Combine(_xx1,handler); 10 } 11 } 12 public void remove_XX1(XXEventHandler handler) 13 { 14 lock(this) 15 { 16 _xx1 = (XXEventHandler)Delegate.Remove(_xx1,handler); 17 } 18 19 } 20 public void add_XX2(EventHandler handler) 21 { 22 lock(this) 23 { 24 _xx2 = (EventHandler)Delegate.Combine(_xx2,handler); 25 } 26 } 27 public void remove_XX2(EventHandler handler) 28 { 29 lock(this) 30 { 31 _xx2= (EventHandler)Delegate.Remove(_xx2,handler); 32 } 33 } 34 }
在一个线程中执行sub.XX1+=new XXEventHandler(…)(间接调用sub.add_XX1(new XXEventHandler(…)))的时候,完全可以在另一线程中同时执行 sub.XX2+=new EventHandler(…)(间接调用sub.add_XX2(new EventHandler(…)))。_xx1和_xx2两个没有任何联系,访问他们更不需要线程同步。如果这样做了,影响性能效率(编译器自动转换成的代码就是这样子)。
结合以上两点,可以将“升级”部分代码修改为以下,从而可以很好的解决“线程安全”问题而且不会像编译器自动转换的代码那样影响效率:
1 Class Subject 2 { 3 private XXEventHandler _xx; 4 private object _xxSync = new object(); 5 6 public event XXEventHandler XX 7 { 8 add 9 { 10 lock(_xxSync) 11 { 12 _xx = (XXEventHandler)Delegate.Combine(_xx,value); 13 } 14 } 15 remove 16 { 17 lock(_xxSync) 18 { 19 _xx = (XXEventHandler)Delegate.Remove(_xx,value); 20 } 21 } 22 } 23 protected virtual void OnXX(XXEventArgs e) 24 { 25 if(_xx!=null) 26 { 27 _xx(this,e); 28 } 29 } 30 public void DoSomething() 31 { 32 //符合某一条件 33 OnXX(new XXEventArgs()); 34 } 35 }
在Subject类中增加一个同步锁目标“_xxSync”,不再以对象本身为同步锁目标,这样_xxSync只在类内部可见(客户端代码不可使用该对象作为同步锁目标),不会出现死锁现象。另外,如果Subject有多个事件,那么我们可以完全增加多个类似“_xxSync”这样的东西,比如“_xx1Sync、_xx2Sync…”等等,每个同步锁目标之间没有任何关联。
当一个类(比如前面提到的Subject)中包含的事件增多时,几十个甚至几百个,而且派生类还会增加事件,在这种情况下,我们需要统一管理这些事件,由一个集合来统一管理这些事件是个不错的选择,比如:
1 Class Subject 2 { 3 protected Dictionary<object,Delegate> _handlerList = new Dictionary<object,Delegate>(); 4 Static object _XX1_KEY = new object(); 5 Static object _XX2_KEY = new object(); 6 Static object _XXn_KEY = new object(); 7 8 //事件 9 public event EventHandler XX1 10 { 11 add 12 { 13 if(_handlerList.ContainsKey(_XX1_KEY)) 14 { 15 _handlerList[_XX1_KEY] = Delegate.Combine(_handlerList[_XX1_KEY],value); 16 } 17 else 18 { 19 _handlerList.Add(_XX1_KEY,value); 20 } 21 } 22 remove 23 { 24 if(_handlerList.ContainsKey(_XX1_KEY)) 25 { 26 _handlerList[_XX1_KEY] = Delegate.Remove(_handlerList[_XX1_KEY],value); 27 } 28 } 29 } 30 public event EventHandler XX2 31 { 32 add 33 { 34 if(_handlerList.ContainsKey(_XX2_KEY)) 35 { 36 _handlerList[_XX2_KEY] = Delegate.Combine(_handlerList[_XX2_KEY],value); 37 } 38 else 39 { 40 _handlerList.Add(_XX2_KEY,value); 41 } 42 } 43 remove 44 { 45 if(_handlerList.ContainsKey(_XX2_KEY)) 46 { 47 _handlerList[_XX2_KEY] = Delegate.Remove(_handlerList[_XX2_KEY],value); 48 } 49 } 50 } 51 public event EventHandler XXn 52 { 53 add 54 { 55 if(_handlerList.ContainsKey(_XXn_KEY)) 56 { 57 _handlerList[_XXn_KEY] = Delegate.Combine(_handlerList[_XXn_KEY],value); 58 } 59 else 60 { 61 _handlerList.Add(_XXn_KEY,value); 62 } 63 64 } 65 remove 66 { 67 if(_handlerList.ContainsKey(_XXn_KEY)) 68 { 69 _handlerList[_XXn_KEY] = Delegate.Remove(_handlerList[_XXn_KEY],value); 70 } 71 72 } 73 } 74 protected virtual void OnXX1(EventArgs e) 75 { 76 if(_handlerList.ContainsKey(_XX1_KEY)) 77 { 78 EventHandler handler = _handlerList[_XX1_KEY] as EventHandler; 79 If(handler != null) 80 { 81 Handler(this,e); 82 } 83 } 84 } 85 protected virtual void OnXX2(EventArgs e) 86 { 87 if(_handlerList.ContainsKey(_XX2_KEY)) 88 { 89 EventHandler handler = _handlerList[_XX2_KEY] as EventHandler; 90 if(handler != null) 91 { 92 Handler(this,e); 93 } 94 } 95 } 96 protected virtual void OnXXn(EventArgs e) 97 { 98 if(_handlerList.ContainsKey(_XXn_KEY)) 99 { 100 EventHandler handler = _handlerList[_XXn_KEY] as EventHandler; 101 If(handler != null) 102 { 103 Handler(this,e); 104 } 105 } 106 } 107 108 public void DoSomething() 109 { 110 //符合某一条件 111 OnXX1(new EventArgs()); 112 OnXX2(new EventArgs()); 113 OnXXn(new EventArgs()); 114 } 115 }
存放事件委托链表的容器为Dictionary<object,Delegate>类型,该容器存放各个委托链表的表头,每当有一个“事件注册”的动作发生时,先查找字典中是否有表头,如果有,直接加到表头后面;如果没有,向字典中新加一个表头。“事件注销”操作类似。
图1
字典的作用是将每个委托链表的表头组织起来,便于查询访问。可能有人已经看出来修改后的代码并没有考虑“线程安全”问题,的确,引进了集合去管理委托链表之后,再也没办法解决“线程安全”而又不影响效率了,因为现在各个事件不再是独立存在的,它们都放在了同一集合。另外,集合Dictionary<object,Delegate>声明为protected,子类完全可以使用该集合对子类的事件委托链表进行管理。
注:上图中委托链中各节点引用的都是实例方法,没有列举静态方法。
其实,.net中所有从System.Windows.Forms.Control类继承下来的类,都是用这种方式去维护事件委托链表的,只不过它不是用的字典(我只是用字典模拟),它使用一个EventHandlerList类对象来存储所有的委托链表表头,作用跟Dictionary<object,Delegate>差不多,并且,.net中也没去处理“线程安全”问题。总之,CLR在处理“线程安全”问题做得不是足够好,当然,一般事件编程也基本用在单线程中(比如Winform中的UI线程中),打个比方,在UI线程中创建的Control(或其派生类),基本上都在同一线程中访问它,基本不涉及跨线程去访问Control(或其派生类),所以大可不必担心事件编程中遇到“线程安全”问题。
事件编程中的内存泄露
说到“内存泄露”,可能很多人认为这不应该是.net讨论的问题,因为GC自动回收内存,不需要编程的人去管理内存,其实不然。凡是发生了不能及时释放内存的情况,都可以叫“内存泄露”,.net中包括“托管内存”也包括“非托管内存”,前者由GC管理,后者必然由编程者考虑了(类似C++中的内存),这里我们讨论的是前者,也就是托管内存的泄露。
我们知道(假设诸位都知道),当一个托管堆中的对象不可达时,也就是程序中没有对该对象有引用时,该对象所占堆内存就属于GC回收的范围了。可是,如果编程者认为一个对象生命期应该结束(该对象不再使用)的时候,同时也理所当然地认为GC会回收该对象在堆中占用的内存时,情况往往不是TA所认为的那样,应为很有可能(概率很大),该对象在其他的地方仍然被引用,而且该引用相对来说不会很明显,我们叫这个为“隐式强引用”(Implicit
strong reference),而对于Class A = new Class();这样的代码,A就是“显示强引用”(Explicit strong reference)了。(至于什么是强引用什么是弱引用,这个在这里我就不说了)那么,不管是“显示强引用”还是“隐式强引用”都属于“强引用”,一个对象有一个强引用存在的话,GC就不会对它进行内存回收。
事件编程中,经常会产生“隐式强引用”,参考前面的“图1”中委托链表中的每个节点都包含一个target,当一个事件观察者向发布者注册一个事件时,那么,发布者就会保持一个观察者的强引用,这个强引用不是很明显,因此我们称之为隐式强引用。因此,当观察者被编程者理所当然地认为生命期结束了,再没有任何对它的引用存在时,事件发布者却依然保持了一个强引用。如下图:
图2
尽管有时候,Observer生命期结束(我们理所当然地那样认为),Subject(发布者)却依旧对Observer有一个强引用(strong reference)(图2中红色箭头),该引用称作为“隐式强引用”。GC不会对Observer进行内存回收,因为还有强引用存在。如果Observer为大对象,且系统存在很多这样的Observer,当系统运行时间足够长,托管堆中的“僵尸对象”(有些对象虽然已经没有使用价值了,但是程序中依旧存在对它的强引用)越来越多,总有一个时刻,内存不足,程序崩溃。
事件编程中引起的异常
其实还是因为我们的Observer注册了事件,但在Observer生命期结束(编程者认为的)时,释放了一些必备资源,但是Subject还是对Observer有一个强引用,当事件发生后,Subject还是会通知Observer,如果Observer在处理事件的时候,也就是事件处理程序中用到了之前已经释放了的“必备资源”,程序就会出错。导致这个异常的原因就是,编程者以为对象已经死了,将其资源释放,但对象本质上还未死去,仍然会处理它注册过的事件。
1 //Form1.cs中: 2 private void form1_Load(object sender,EventArgs e) 3 { 4 Form2 form2 = new Form2(); 5 form2.Click += new EventHandler(form2_Click); 6 form2.Show(); 7 } 8 private void form2_Click(object sender,EventArgs e) 9 { 10 this.Show(); 11 }
form1为Observer,form2为Subject,form1监听form2的Click事件,在事件处理程序中将自己Show出来,一切运行良好,但是,当form1关闭后,再次点击form2激发Click事件时,程序报错,提示form1已经disposed。原因就是我们关闭form1时,认为form1生命期已经结束了,事实上并非如此,form2中还有对form1的引用,当事件发生后,还是会通知form1,调用form1的事件处理程序(form2_Click),而碰巧的是,事件处理程序中调用了this.Show()方法,意思要将form1显示出来,可此时form1已经关闭了。
小结
不管是内存泄露还是引起的异常,都是因为我们注册了某些事件,在对象生命期结束时,没有及时将已注册的事件注销,告诉事件发布者“我已死,请将我的引用删除”。因此一个简单的方法就是在对象生命期结束时将所有的事件注销,但这个只对简单的代码结构有效,复杂的系统几乎无效,事件太多,根本无法记录已注册的事件,再者,你有时候根本不知道对象什么时候生命期结束。下次介绍利用弱引用概念(Weak reference)引申出来的弱委托(Weak delegate),它能有效地解决事件编程中内存泄露问题。原理就是将图2中每个节点中的Target由原来的强引用(Strong
Reference)改为弱引用(Weak Reference)。
MSDN上对Delegate(委托)的解释:
表示委托,委托是一种数据结构,它引用静态方法或引用类实例及该类的实例方法。
我们先不去管网上对“委托”的其他形象比如,比如“类似函数指针”、“对同一类方法的签名”等等。先来看看MSDN上的解释是个什么意思。首先它是一种数据结构,基本可以看做包含两个东西,一个是方法,一个是方法的所有者,即对象(静态方法的所有者为类)。那么我们可以简单的用图来画一下:
图1
如果对委托比较熟悉的人可能已经知道,所有我们定义的委托变量,都有两个属性Target和Method,这也正验证了上图的正确性。
那么Event(事件)是个什么东西?看一下MSDN对“事件”的一段解释:
在发生其他类或对象关注的事情时,类或对象可通过事件通知它们。发送(或引发)事件的类称为“发行者”,接收(或处理)事件的类称为“订户”。
这个解释基本上没什么用,因为它跟之前谈到的“委托”没任何联系,那我们再回忆一下我们之前在代码中是怎么去定义一个事件的?书上说网上也说,定义一个委托变量,在声明前面加一个event关键字,event关键字小写,那么这个委托变量就可以叫事件,编写代码时,VS IDE智能提示会在事件前面有一个“闪电”的图标。那么,根据这个描述,事件明显就是一种特殊的委托实例,没错,事件就是一种特殊的委托实例,它具备所有委托所具备的特点(有一些限制,跟主题没关系,略过)。
当事件的观察者(Observer)向事件的发布者(Subject)注册一个事件时,事件发布者就会产生一个对事件观察者的强引用(strong reference)(不然,当事件发生时,怎么才能通知观察者),也就是说,“注册事件”这个动作将事件的发布者和事件的观察者绑定到了一起,具体体现在:发布者包含了一个对观察者的强引用。
结合上一篇文章中的“图2”,我们可以看出,一个事件对应一个委托链,激发事件的过程可以看做:遍历这个委托链,在每一个节点中的Target上调用Method。这就完成了“事件发布者->激发事件->通知观察者->观察者响应事件->调用事件处理程序”这一过程。那么,对于一个事件,每被注册一次,它对应的委托链上就应该增加一个节点,该节点中存储着观察者的信息,由于同一个事件,同一个对象可能注册好几次,因此某一事件对应委托链中的节点的个数并不等于该事件的观察者数目,一般前者大于后者。再来看一张图:
图2
由此看出,委托链维持着事件发布者与事件观察者之间的联系,在某些情况下,当我们认为我们程序中的观察者已死(代码中再没有对观察者的引用),GC会回收观察者的内存时,实际情况往往不是我们认为的这样,由于委托链中仍然有对观察者的强引用,GC是不会对它进行内存回收的,这样一来,如果观察者数目大,观察者属于大对象,系统运行时间过长,就会导致内存不足。
注:GC回收对象内存的前提是程序中不再有该对象的强引用(strong reference)。
解决以上问题关键在于,怎么在观察者即将死亡的时候通知发布者将自己的引用删除,但是这个几乎做不到,因为你根本不知道观察者什么时候死亡,再者,当系统复杂庞大之后,你根本不知道谁是谁的观察者,谁又暗自拥有谁的强引用。所以,最好的情况是,观察者在必要的时候,能够正常的被GC回收内存,对于事件发布者来说,观察者的存在与否没有多大关系,如果观察者还存在,那么当事件发生时就通知观察者,如果观察者不存在了,那么当事件发生时就不通知观察者。这就要求事件发布者对于观察者之间的关联不能像之前那种“强引用”的关系,而应该是“弱引用”的关系。
因此,我们可以仿照原有的Delegate数据结构,模拟一种新的数据结构,将原来Target这个“强引用”变为“弱引用”,我们取名叫做“弱委托”(WeakDelegate),代码类似如下:
1 class WeakDelegate 2 { 3 WeakReference _weakTarget; 4 MethodInfo _method; 5 public WeakDelegate(WeakReference weakTarget, MethodInfo method) 6 { 7 _weakTarget = weakTarget; 8 _method = method; 9 } 10 public WeakReference WeakTarget 11 { 12 get 13 { 14 return _weakTarget; 15 } 16 } 17 public MethodInfo Method 18 { 19 get 20 { 21 return _method; 22 } 23 } 24 public object Invoke(params object[] parameters) 25 { 26 try 27 { 28 return _method.Invoke(_weakTarget.Target, parameters); 29 } 30 catch 31 { 32 return null; 33 } 34 } 35 }
包含两个属性WeakTarget和Method,分别对应原来委托的Target和Method属性,一个Invoke公共方法,对应原来委托的Invoke方法,用于执行委托。
由此一来,图2中委托链中的各个节点就可以由原来的Delegate(其派生类,我们自己定义的委托)变为我们现在的WeakDelegate,那么在写事件发布者类(Subject)的代码时,我们需要自己来维持这些委托链了,如下:
1 class Subject 2 { 3 protected Dictionary<object, List<WeakDelegate>> _delegateList = new Dictionary<object, List<WeakDelegate>>(); 4 5 private static object _XX1_KEY = new object(); 6 private static object _XX2_KEY = new object(); 7 private static object _XXn_KEY = new object(); 8 9 public event EventHandler XX1 10 { 11 add 12 { 13 if (_delegateList.ContainsKey(_XX1_KEY)) 14 { 15 _delegateList[_XX1_KEY].Add(new WeakDelegate(new WeakReference(value.Target), value.Method)); 16 } 17 else 18 { 19 List<WeakDelegate> list = new List<WeakDelegate>(); 20 list.Add(new WeakDelegate(new WeakReference(value.Target), value.Method)); 21 _delegateList.Add(_XX1_KEY, list); 22 } 23 } 24 remove 25 { 26 if (_delegateList.ContainsKey(_XX1_KEY)) 27 { 28 List<WeakDelegate> list = _delegateList[_XX1_KEY]; 29 List<WeakDelegate> del = new List<WeakDelegate>(); 30 foreach (WeakDelegate d in list) 31 { 32 WeakReference w = d.WeakTarget; 33 object tmp = w.Target; //先引用到w中的Target,因为GC是在自己的线程中回收垃圾 34 if (w.IsAlive) //再判断w.Target是否已被GC回收 35 { 36 if (w.Target == value.Target && d.Method == value.Method) 37 { 38 del.Add(d); 39 } 40 } 41 } 42 if (del.Count != 0) 43 { 44 foreach (WeakDelegate d in del) 45 { 46 list.Remove(d); 47 } 48 del.Clear(); 49 } 50 del = null; 51 } 52 } 53 } 54 public event EventHandler XX2 55 { 56 add 57 { 58 if (_delegateList.ContainsKey(_XX2_KEY)) 59 { 60 _delegateList[_XX2_KEY].Add(new WeakDelegate(new WeakReference(value.Target), value.Method)); 61 } 62 else 63 { 64 List<WeakDelegate> list = new List<WeakDelegate>(); 65 list.Add(new WeakDelegate(new WeakReference(value.Target), value.Method)); 66 _delegateList.Add(_XX2_KEY, list); 67 } 68 } 69 remove 70 { 71 if (_delegateList.ContainsKey(_XX2_KEY)) 72 { 73 List<WeakDelegate> list = _delegateList[_XX2_KEY]; 74 List<WeakDelegate> del = new List<WeakDelegate>(); 75 foreach (WeakDelegate d in list) 76 { 77 WeakReference w = d.WeakTarget; 78 object tmp = w.Target; //先引用到w中的Target,因为GC是在自己的线程中回收垃圾 79 if (w.IsAlive) //再判断w.Target是否已被GC回收 80 { 81 if (w.Target == value.Target && d.Method == value.Method) 82 { 83 del.Add(d); 84 } 85 } 86 } 87 if (del.Count != 0) 88 { 89 foreach (WeakDelegate d in del) 90 { 91 list.Remove(d); 92 } 93 del.Clear(); 94 } 95 del = null; 96 } 97 } 98 } 99 public event EventHandler XXn 100 { 101 add 102 { 103 if (_delegateList.ContainsKey(_XXn_KEY)) 104 { 105 _delegateList[_XXn_KEY].Add(new WeakDelegate(new WeakReference(value.Target), value.Method)); 106 } 107 else 108 { 109 List<WeakDelegate> list = new List<WeakDelegate>(); 110 list.Add(new WeakDelegate(new WeakReference(value.Target), value.Method)); 111 _delegateList.Add(_XXn_KEY, list); 112 } 113 } 114 remove 115 { 116 if (_delegateList.ContainsKey(_XXn_KEY)) 117 { 118 List<WeakDelegate> list = _delegateList[_XXn_KEY]; 119 List<WeakDelegate> del = new List<WeakDelegate>(); 120 foreach (WeakDelegate d in list) 121 { 122 WeakReference w = d.WeakTarget; 123 object tmp = w.Target; //先引用到w中的Target,因为GC是在自己的线程中回收垃圾 124 if (w.IsAlive) //再判断w.Target是否已被GC回收 125 { 126 if (w.Target == value.Target && d.Method == value.Method) 127 { 128 del.Add(d); 129 } 130 } 131 } 132 if (del.Count != 0) 133 { 134 foreach (WeakDelegate d in del) 135 { 136 list.Remove(d); 137 } 138 del.Clear(); 139 } 140 del = null; 141 } 142 } 143 } 144 145 public event EventHandler CommonEvent; 146 147 protected virtual void OnXX1(EventArgs e) 148 { 149 if (_delegateList.ContainsKey(_XX1_KEY)) //有事件注册者 150 { 151 List<WeakDelegate> list = _delegateList[_XX1_KEY]; 152 List<WeakDelegate> del = new List<WeakDelegate>(); 153 foreach (WeakDelegate d in list) //遍历委托链(集合) 154 { 155 object tmp = d.WeakTarget.Target; // 156 157 if (d.WeakTarget.IsAlive) //查找是否已被GC回收 158 { 159 d.Invoke(new object[] { this, e }); //通知观察者 160 } 161 else 162 { 163 del.Add(d); //添加到待删集合 164 } 165 } 166 if (del.Count != null) 167 { 168 foreach (WeakDelegate d in del) //移除已被GC回收的观察者 169 { 170 list.Remove(d); 171 } 172 } 173 } 174 } 175 protected virtual void OnXX2(EventArgs e) 176 { 177 if (_delegateList.ContainsKey(_XX2_KEY)) //有事件注册者 178 { 179 List<WeakDelegate> list = _delegateList[_XX2_KEY]; 180 List<WeakDelegate> del = new List<WeakDelegate>(); 181 foreach (WeakDelegate d in list) //遍历委托链(集合) 182 { 183 object tmp = d.WeakTarget.Target; // 184 185 if (d.WeakTarget.IsAlive) //查找是否已被GC回收 186 { 187 d.Invoke(new object[] { this, e }); //通知观察者 188 } 189 else 190 { 191 del.Add(d); //添加到待删集合 192 } 193 } 194 if (del.Count != null) 195 { 196 foreach (WeakDelegate d in del) //移除已被GC回收的观察者 197 { 198 list.Remove(d); 199 } 200 } 201 } 202 } 203 protected virtual void OnXXn(EventArgs e) 204 { 205 if (_delegateList.ContainsKey(_XXn_KEY)) //有事件注册者 206 { 207 List<WeakDelegate> list = _delegateList[_XXn_KEY]; 208 List<WeakDelegate> del = new List<WeakDelegate>(); 209 foreach (WeakDelegate d in list) //遍历委托链(集合) 210 { 211 object tmp = d.WeakTarget.Target; // 212 213 if (d.WeakTarget.IsAlive) //查找是否已被GC回收 214 { 215 d.Invoke(new object[] { this, e }); //通知观察者 216 } 217 else 218 { 219 del.Add(d); //添加到待删集合 220 } 221 } 222 if (del.Count != null) 223 { 224 foreach (WeakDelegate d in del) //移除已被GC回收的观察者 225 { 226 list.Remove(d); 227 } 228 } 229 } 230 } 231 protected virtual void OnCommonEvent(EventArgs e) 232 { 233 if (CommonEvent != null) 234 { 235 CommonEvent(this, e); 236 } 237 } 238 239 public void DoSomething() 240 { 241 //符合某一条件,激发事件 242 OnXX1(new EventArgs()); 243 OnXX2(new EventArgs()); 244 OnXXn(new EventArgs()); 245 OnCommonEvent(new EventArgs()); 246 } 247 }
在事件发布者中,我定义了三个使用我们自己WeakDelegate的事件,由_delegateList管理,分别是XX1、XX2、XXn事件,他们对应的委托链中的每一个节点属于WeakDelegate类型,不会保存观察者的强引用。另外为了对比,我定义了一个传统的使用Delegate的事件CommonEvent,它对应的委托链中的每一个节点属于Delegate(派生类)类型,保存观察者的强引用。
定义了一个观察者(Observer)类,代码为:
1 class Observer 2 { 3 public void RegisterXX1(Subject sub) 4 { 5 sub.XX1 += new EventHandler(sub_XX1); 6 } 7 public void RegisterXX2(Subject sub) 8 { 9 sub.XX2 += new EventHandler(sub_XX2); 10 } 11 public void RegisterXXn(Subject sub) 12 { 13 sub.XXn += new EventHandler(sub_XXn); 14 } 15 public void RegisterCommonEvent(Subject sub) 16 { 17 sub.CommonEvent += new EventHandler(sub_CommonEvent); 18 } 19 20 void sub_CommonEvent(object sender, EventArgs e) 21 { 22 Console.WriteLine("Common Event raised!"); 23 } 24 25 void sub_XXn(object sender, EventArgs e) 26 { 27 Console.WriteLine("XXn event raised!"); 28 } 29 void sub_XX2(object sender, EventArgs e) 30 { 31 Console.WriteLine("XX2 event raised!"); 32 } 33 34 void sub_XX1(object sender, EventArgs e) 35 { 36 Console.WriteLine("XX1 event raised!"); 37 } 38 }
该类除了注册Subject类的事件外,没有其他内容,当事件发生后,输出与该事件相关的信息。
测试代码如下:
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 //Subject sub = new Subject(); //事件发布者 6 //Observer obs = new Observer(); //事件观察者 7 //obs.RegisterXX1(sub); //观察者注册事件XX1,两者建立关联 8 9 10 //sub.DoSomething(); //发布者激发事件 11 ////输出XX1 event raised! 12 13 14 ////强制GC回收obs内存,结束obs的生命期 15 //obs = null; 16 //GC.Collect(); 17 //Console.WriteLine("obs is 'dead'?!"); 18 19 20 //sub.DoSomething(); //再次激发事件 21 ////obs已死,不会再响应XX1事件 22 23 24 //Console.Read(); 25 26 27 Subject sub = new Subject(); //事件发布者 28 Observer obs = new Observer(); //事件观察者 29 obs.RegisterXX1(sub); //观察者注册事件XX1,建立关联 30 obs.RegisterCommonEvent(sub); //观察者注册事件CommonEvent,建立关联 31 32 33 sub.DoSomething(); //事件发布者激发事件 34 //输出 XX1 event raised! 35 // CommonEvent event raised! 36 37 38 //强制GC回收obs内存,结束obs的生命期,但是徒劳!!! 39 obs = null; 40 GC.Collect(); 41 Console.WriteLine("obs is 'dead'?!"); 42 43 44 sub.DoSomething(); //再次激发事件 45 //obs并没有死去,会再次响应事件 46 //输出 XX1 event raised! 47 // CommonEvent event raised! 48 49 50 //因此,obs并没有我们期待的那样结束了自己的生命期 51 Console.Read(); 52 } 53 }
被注释部分事件发布者与观察者之间属于弱关联,观察者能够正常死亡;未被注释部分事件发布者与观察者属于强关联,观察者不能正常死亡。
运行结果如下:
图3
图4
相关文章推荐
- .net委托与事件解析
- .Net中的事件与委托
- 庖丁解牛——深入解析委托和事件
- .NET之美——C# 中的委托和事件
- .Net中事件与委托的示例详细解
- .net的委托和事件的直接理解[转载]
- 复习一下 .Net: delegate(委托)、event(事件) 的基础知识,从头到尾实现事件!
- .net 中的委托和事件(适合初学者,看后绝对会领会不少)
- .Net: C#中的委托(Delegate)和事件(Event)
- .Net委托类型解析
- .Net中使用事件和委托实现Observer模式(一)
- .net中的委托和事件
- .NET基础拾遗(4)委托、事件、反射与特性
- .NET中常见的内存泄露问题——GC、委托事件和弱引用
- .net的事件与委托机制
- .NET 使用委托和事件(二)
- .net的委托和事件的直接理解
- .Net委托机制之事件委托
- .NET基础拾遗(4)委托和事件2
- .Net事件&委托备忘