我前面几篇博客中提到过.net中的事件与Windows事件的区别,本文讨论的是前者,也就是我们代码中经常用到的Event。Event很常见,Button控件的Click、KeyPress等等,PictureBox控件的Paint等等都属于本文讨论范畴,本文会例举出有关“事件编程”的几种方法,还会提及由“事件编程”引起的MemoryLeak(跟“内存泄露”差不多),以及由“事件编程”引起的一些异常。
引子:
.net中事件最常用在“观察者”设计模式中,事件的发布者(subject)定义一个事件,事件的观察者(observer)注册这个事件,当发布者激发该事件时,所有的观察者就会响应该事件(表现为调用各自的事件处理程序)。知道这个逻辑过程后,我们可以写出以下代码:
复制代码 代码如下:
ViewCode
ClassSubject
{
publiceventXXEventHandlerXX;
protectedvirtualvoidonXX(XXEventArgse)
{
If(XX!=null)
{
XX(this,e);
}
}
publicvoidDoSomething()
{
//符合某一条件
onXX(newXXEventArgs());
}
}
delegatevoidXXEventHandler(objectsender,XXEventArgse);
ClassXXEventArgs:EventArgs
{
}
以上就是一个最最原始的含有事件类的定义。外部对象可以注册Subject对象的XX事件,当某一条件满足时,Subject对象就会激发XX事件,所以观察者作出响应。
注:编码中请按照标准的命名方式,事件名、事件参数名、虚方法名、参数名等等,标准请参考微软。
事件观察者注册事件代码为:
复制代码 代码如下:
ViewCode
Subjectsub=newSubject();
Sub.XX+=newXXEventHandler(sub_XX);
voidsub_XX(objectsender,XXEventArgse)
{
//dosomething
}
以上是一个最简单的“事件编程”结构代码,其余所有的写法都是从以上扩展出来的,基本原理不变。
升级:
在定义事件变量时,有时候我们可以这样写:
复制代码 代码如下:
ViewCode
ClassSubject
{
privateXXEventHandler_xx;
publiceventXXEventHandlerXX
{
add
{
_xx=(XXEventHandler)Delegate.Combine(_xx,value);
}
remove
{
_xx=(XXEventHandler)Delegate.Remove(_xx,value);
}
}
protectedvirtualvoidonXX(XXEventArgse)
{
if(_xx!=null)
{
_xx(this,e);
}
}
publicvoidDoSomething()
{
//符合某一条件
onXX(newXXEventArgs());
}
}
其余代码跟之前一样,升级后的代码显示的实现了“add/remove”,显示实现“add/remove”的好处网上很多人都说可以在注册事件之前添加额外的逻辑,这个就像“属性”和“字段”的关系,
复制代码 代码如下:
ViewCode
publiceventXXEventHandlerXX
{
add
{
//添加逻辑
_xx=(XXEventHandler)Delegate.Combine(_xx,value);
}
remove
{
//添加逻辑
_xx=(XXEventHandler)Delegate.Remove(_xx,value);
}
}
没错,确实与“属性(Property)”的作用差不多,但它不止这一个好处,我们知道(不知道的上网看看),在多线程编程中,很重要的一点就是要保证对象“线程安全”,因为多线程同时访问同一资源时,会出现预想不到的结果。当然,在“事件编程”中也要考虑多线程的情况。“引子”部分代码经过编译器编译后,确实可以解决多线程问题,但是存在问题,它经过编译后:
复制代码 代码如下:
ViewCode
publiceventXXEventHandlerXX;
//该行代码编译后类似如下:
privateXXEventHandler_xx;
[MethodImpl(MethodImplOptions.Synchronized)]
publicvoidadd_XX(XXEventHandlerhandler)
{
_xx=(XXEventHandler)Delegate.Combine(_xx,handler);
}
[MethodImpl(MethodImplOptions.Synchronized)]
publicvoidremove_XX(XXEventHandlerhandler)
{
_xx=(XXEventHandler)Delegate.Remove(_xx,handler);
}
以上转换为编译器自动完成,事件(取消)注册(+=、-=)间接转换由add_XX和remove_XX代劳,通过在add_XX方法和remove_XX方法前面添加类似[MethodImpl(MethodImplOptions.Synchronized)]声明,表明该方法为同步方法,也就是说多线程访问同一Subject对象时,同时只能有一个线程访问add_XX或者是remove_XX,这就确保了不可能同时存在两个线程操作_xx这个委托链表,也就不可能发生不可预测结果。那么,[MethodImpl(MethodImplOptions.Synchronized)]是怎么做到线程同步的呢?其实查看IL语言,我们不难发现,[MethodImpl(MethodImplOptions.Synchronized)]的作用类似于下:
复制代码 代码如下:
ViewCode
ClassSubject
{
privateXXEventHandler_xx;
publicvoidadd_XX(XXEventHandlerhandler)
{
lock(this)
{
_xx=(XXEventHandler)Delegate.Combine(_xx,handler);
}
}
publicvoidremove_XX(XXEventHandlerhandler)
{
lock(this)
{
_xx=(XXEventHandler)Delegate.Remove(_xx,handler);
}
}
}
如我们所见,它就相当于给自己加了一个同步锁,lock(this),我不知道诸位在使用同步锁的时候有没有刻意去避免lock(this)这种,我要说的是,使用这种同步锁要谨慎。原因至少两个:
1)将自己(Subject对象)作为锁定目标的话,客户端代码中很可能仍以自己为目标使用同步锁,造成死锁现象。因为this是暴露给所有人的,包括代码使用者。
复制代码 代码如下:
ViewCode
privatevoidDoWork(Subjectsub)//客户端代码
{
lock(sub)//客户端代码锁定sub对象
{
sub.XX+=newXXEventHandler(…);//嵌套锁定同一目标
//sub.add_XX(newXXEventHandler(…));相当于调用add_XX,出现死锁
//
//
//
//dootherthing
}
}
2)当Subject类包含多个事件,XX1、XX2、XX3、XX4…时,每注册(或取消)一个事件时,都需要锁定同一目标(Subject对象),这完全没必要。因为不同的事件有不同的委托链表,多个线程完全可以同时访问不同的委托链表。然而,编译器还是这样做了。
复制代码 代码如下:
ViewCode
ClassSubject
{
privateXXEventHandler_xx1
privateEventHandler_xx2;
publicvoidadd_XX1(XXEventHandlerhandler)
{
lock(this)
{
_xx1=(XXEventHandler)Delegate.Combine(_xx1,handler);
}
}
publicvoidremove_XX1(XXEventHandlerhandler)
{
lock(this)
{
_xx1=(XXEventHandler)Delegate.Remove(_xx1,handler);
}
}
publicvoidadd_XX2(EventHandlerhandler)
{
lock(this)
{
_xx2=(EventHandler)Delegate.Combine(_xx2,handler);
}
}
publicvoidremove_XX2(EventHandlerhandler)
{
lock(this)
{
_xx2=(EventHandler)Delegate.Remove(_xx2,handler);
}
}
}
在一个线程中执行sub.XX1+=newXXEventHandler(…)(间接调用sub.add_XX1(newXXEventHandler(…)))的时候,完全可以在另一线程中同时执行sub.XX2+=newEventHandler(…)(间接调用sub.add_XX2(newEventHandler(…)))。_xx1和_xx2两个没有任何联系,访问他们更不需要线程同步。如果这样做了,影响性能效率(编译器自动转换成的代码就是这样子)。
结合以上两点,可以将“升级”部分代码修改为以下,从而可以很好的解决“线程安全”问题而且不会像编译器自动转换的代码那样影响效率:
复制代码 代码如下:
ViewCode
ClassSubject
{
privateXXEventHandler_xx;
privateobject_xxSync=newobject();
publiceventXXEventHandlerXX
{
add
{
lock(_xxSync)
{
_xx=(XXEventHandler)Delegate.Combine(_xx,value);
}
}
remove
{
lock(_xxSync)
{
_xx=(XXEventHandler)Delegate.Remove(_xx,value);
}
}
}
protectedvirtualvoidonXX(XXEventArgse)
{
if(_xx!=null)
{
_xx(this,e);
}
}
publicvoidDoSomething()
{
//符合某一条件
onXX(newXXEventArgs());
}
}
在Subject类中增加一个同步锁目标“_xxSync”,不再以对象本身为同步锁目标,这样_xxSync只在类内部可见(客户端代码不可使用该对象作为同步锁目标),不会出现死锁现象。另外,如果Subject有多个事件,那么我们可以完全增加多个类似“_xxSync”这样的东西,比如“_xx1Sync、_xx2Sync…”等等,每个同步锁目标之间没有任何关联。
当一个类(比如前面提到的Subject)中包含的事件增多时,几十个甚至几百个,而且派生类还会增加事件,在这种情况下,我们需要统一管理这些事件,由一个集合来统一管理这些事件是个不错的选择,比如:
复制代码 代码如下:
ViewCode
ClassSubject
{
protectedDictionary



