C#事件浅析

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

C#事件浅析
前⾔
对于搞.net的朋友来说,经常会遇到关于事件和委托的问题:事件与委托有什么关系?事件的本质是什么?委托的本质⼜是什么?由于.net 做了⼤量的封装,对于初学者,这两个概念确实不怎么好理解。

事件是⽤户与应⽤程序交互的基础,它是回调机制的⼀种应⽤。

举个例⼦,当⽤户点击按钮时,我们希望弹出⼀句“您好”;这⾥的【点击】就是⼀个事件;那么回调就是我们注册⼀个⽅法,当⽤户点击时,程序⾃动执⾏这个⽅法去响应这个操作,⽽不是我们时刻去监听⽤户有没有点击。

上⼀篇已经介绍了的相关知识,它就是.net⽤来实现回调机制的技术,⽽事件⼜是回调机制的⼀种应⽤,在学习事件前,应该先学习好的知识。

有了委托的基础,接下来就让我们⼀步步⾛进事件。

⼀、使⽤事件
假设场景:有⼀个邮件管理员(事件拥有者),它赋值接收邮件,当邮件到来时(事件),邮件可以交给传真员和打印员处理(事件订阅者)。

很明显,邮件到来的时间只有邮件管理员知道,传真员和打印员也不可能时刻去询问有没有新邮件,⽽是应该由管理员主动来通知(回调),但他们也要先告诉管理员,新邮件到来时,我需要处理(订阅)。

这⾥接收新邮件就是⼀个事件,邮件有⼀定的信息(事件附加信息),例如:发送⼈、接收⼈,内容。

接下来我们通过这个过程来了解事件。

1. 定义事件所需要的附加信息
按照约定,所有的事件的附加信息都应该从EventArgs派⽣,因为我们希望⼀看就知道这是个事件附加信息参数,⽽不是其它的。

EventArgs的定义如下:
public class EventArgs {
public static readonly EventArgs Empty = new EventArgs();
public EventArgs() {}
}
可以看到,该类有⼀个静态只读字段Empty,这是⼀个单例;与String.Empty⼀样,当我们需要⼀个空的EventArgs时,应该使⽤EventArgs.Empty,⽽不是重新去new⼀个。

我们定义⼀个NewMailEventArgs参数,如下:
class NewMailEventArgs : EventArgs
{
private string from;
private string to;
private string content;
public string From { get { return from; } }
public string To { get { return to; } }
public string Content { get { return content; } }
public NewMailEventArgs(string from,string to,string content)
{
this.from = from;
this.to = to;
this.content = content;
}
}
2. 定义事件
c#⾥定义事件⽤到了event关键字,⽽且事件⼀般都是公开类型的。

我们定义⼀个NewMail事件如下:
public event EventHandler<NewMailEventArgs> NewMail;
我们说NewMail是⼀个事件,但.net并没有事件这种类型。

实际上,这⾥它是⼀个EventHandler<TEventArgs>委托(委托⼜是引⽤类型),只不过⽤了event进⾏修饰,也可以说它是⼀种具有事件性质的委托。

我们知道委托是⽤来包装回调函数的,它的本质是⼀个class,回调函数的签名必须与委托的签名⼀致。

⼀个事件可以有多个处理函数,⼀个函数就会被包装成⼀个委托对象,所有的委托对象都保存在NewMail的委托链当中。

所以,触发NewMail事件,其实就是遍历其指向的委托对象的委托链,执⾏每个委托对象所包装的⽅法。

(不清楚可以看)
EventHandler 是个泛型委托,他的定义如下:
public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e);
 这⾥有两个特点:1. 返回值是void,这是因为事件处理函数⼀般不需要有返回值,.net中⼤部分的事件处理函数都是没有返回值的。

2. object类型的sender参数,这表⽰事件的拥有者;这也很符合逻辑,我们除了要拿到实际的附加信息外,还要知道事件是从哪⾥来的;⾄于为什么是object类型的,是因为这样可以给更多的类型使⽤。

3. 定义事件触发函数
//3.定义触发事件的⽅法
protected virtual void OnNewMail(NewMailEventArgs e)
{
/* 第1种做法
if(this.NewMail != null)
{
this.NewMail(this,e);
}
*/
/* 第⼆种做法
EventHandler<NewMailEventArgs> temp = this.NewMail;
if (temp != null)
{
temp(this, e);
}
*/
//第三种做法
EventHandler<NewMailEventArgs> temp = pareExchange(ref this.NewMail, null, null);
if (temp != null)
{
temp(this, e);
}
}
第⼀种做法是很常见的做法,判断不为空,然后就触发。

CLR⾥提到这是线程不安全的做法,因为单我们判断不为空后,准备执⾏时,另⼀个线程将从委托链将委托移除,此时变成了空,引发NullReferenceException异常。

第⼆、三种做法都是线程安全的,因为它通过⼀个临时委托变量(委托链保存了所有委托),通过上⼀篇对委托链的了解,我们知道对委托链进⾏Combine/Remove实际都会创建⼀个新的数组对象,此时对temp没有影响。

但实际上事件主要在单线程的环境下使⽤,所以⼀般也不会出现这种问题。

4. 包装好事件参数,调⽤事件触发函数。

public void ReceiveNewMail(string from, string to, string content)
{
NewMailEventArgs e = new NewMailEventArgs(from, to, content);
OnNewMail(e);
}
接下来,对该事件感兴趣的,就可以对该事件进⾏注册。

class Fax
{
public Fax(MailManager mm)
{
mm.NewMail += FaxMsg;
}
private void FaxMsg(object sender, NewMailEventArgs e)
{
Console.WriteLine(string.Format("fax receive,from:{0} to:{1} content is:{2}", e.From, e.To, e.Content));
}
}
⼆、事件揭秘
前⾯我们已经提到事件的本质是委托,或者说是委托的⼀种应⽤。

要深⼊理解事件,我们通过ILDasm.exe查到定义事件⽽⽣成的代码。

public event EventHandler<NewMailEventArgs> NewMail;
可以看到当我们定义⼀个NewEvent时,编译器帮我们⽣成了:1. ⼀个private NewMail 字段,类型为 EventHandler<NewMailEventArgs>。

2.⼀个 add_NewMail ⽅法,⽤于将委托添加到委托链(内部调⽤了bine⽅法)。

3.⼀个 remove_NewMail ⽅法,⽤于将委托从委托链移除(内部调⽤了Delegate.Remove⽅法)。

对事件的操作,就是是对NewMail字段的操作。

现在我们知道了,事件的本质就是委托,定义事件就是定义委托。

只不过编译器隐藏了这个过程。

那为什么不直接使⽤委托呢?
三、为什么不直接⽤委托
我们知道,事件的本质是委托,那⽤事件实现的地⽅,⽤委托也完成可以实现。

上⾯的代码,我们完全可能这样写来达到相同的⽬的:
class MailManager
{
public EventHandler<EventArgs> NewMail;
public void RaiseNewMail()
{
if (NewMail != null)
{
NewMail(this, EventArgs.Empty);
}
}
}
外部调⽤:
class Fax
{
public Fax(MailManager mm)
{
mm.NewMail += FaxNewMail;
}
public void FaxNewMail(object sender, EventArgs e)
{
Console.WriteLine("Fax New Mail 处理成功");
}
}
对于维护对象状态的字段我们往往不设计为公开类型,因为外部完全可以随意改变它,这不是我们想看到的。

例如上⾯那样写,我们可以在外部直接就调⽤NewMail的Invoke⽅法。

⽽且对于字段,我们⽆法控制具体的获取和设置过程,要控制就需要定义⼀个Get_ ⽅法,⼀个Set_⽅法,对于委托类型来说,就是Add_和Remove_。

对于每个事件,都去定义Add_/Remove_是⾮常⿇烦的。

说到这⾥我们会马上连想到属性的设计,没错,属性是⽤Get_/Set_⽅法提供访问私有字段(⾮委托)的⽅法,事件就是⽤Add_/Remove_⽅法提供访问私有委托字段的⽅法。

四、显⽰实现事件
我们知道隐式实现属性时,编译器会为我们⽣成⼀个private的字段,例如:public string Name{get;set;} 会⾃动⽣成⼀个 _name字段。

但是我们显⽰实现时,编译器就不会为⽣成了。

例如下⾯的写法:
public string Name
{
get{return"Tom";}
set{}
}
对于事件来说显⽰实现就是:
private EventHandler<NewMailEventArgs> _newMail;
public event EventHandler<NewMailEventArgs> NewMail
{
add
{
_newMail += value;
}
remove
{
_newMail -= value;
}
}
Control 的 EventHandlerList
对于 Control 来说,它定义了⼤量的事件(定义事件就是定义委托),⽽这些事件不⼀定都会⽤到,所以这会浪费⼤量的内存。

所以Control ⾥的事件都是显⽰实现的,并且将委托保存在⼀个EventHandlerList集合中,这是⼀个key-value的集合。

这样需要处理哪些事件就只要添加相应的委托即可,看起来像是这样的:
class Control
{
private EventHandlerList events;
protected EventHandlerList Events
{
get
{
if (this.events == null)
{
this.events = new EventHandlerList();
}
return this.events;
}
}
private static readonly object _clickEventObj = new object();
private static readonly object _mouseOverEventObj = new object();
public event EventHandler<EventArgs> Click
{
add
{
this.Events.AddHandler(_clickEventObj, value);
}
remove
{
this.Events.RemoveHandler(_clickEventObj, value);
}
}
public event EventHandler<EventArgs> MouseOver
{
add
{
this.Events.AddHandler(_mouseOverEventObj, value);
}
remove
{
this.Events.RemoveHandler(_mouseOverEventObj, value);
}
}
}
也就是我们针对每个事件定义⼀个 object 类型作为集合的key,虽然会定义许多object,但object的代价⽐委托的要⼩很多。

⾄此我们应该知道:委托的本质是引⽤类型,⽤于包装回调函数,委托⽤于实现回调机制;事件的本质是委托,事件是回调机制的⼀种应⽤。

相关文档
最新文档