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 = Interlocked.CompareExchange(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 方法,用于将委托添加到委托链(内部调用了Delegate.Combine方法)。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的代价比委托的要小很多。
至此我们应该知道:委托的本质是引用类型,用于包装回调函数,委托用于实现回调机制;事件的本质是委托,事件是回调机制的一种应用。
C# 事件浅析的更多相关文章
- js事件浅析
js中关于DOM的操作很多,因此js事件机制也就尤为重要. 事件绑定形式: 一. 内联形式 耦合度高,不利于维护 <button onclick="alert('你点击了这个按钮'); ...
- 移动端WEB开发,click,touch,tap事件浅析
一.click 和 tap 比较 两者都会在点击时触发,但是在手机WEB端,click会有 200~300 ms,所以请用tap代替click作为点击事件. singleTap和doubleTap 分 ...
- web开发,click,touch,tap事件浅析
一.click 和 tap 比较 两者都会在点击时触发,但是在手机WEB端,click会有 200~300 ms,所以请用tap代替click作为点击事件. singleTap和doubleTap 分 ...
- 关于客户端javascript的理解及事件浅析
1,核心JavaScript和客服端JavaScript都有一个单线程执行模型.脚本和事件处理程序在同一时间只能执行一个,没有并发性.这样保持了js编程的简单性. 2,document的定义:一些呈现 ...
- JNI详解---从不懂到理解
转载:https://blog.csdn.net/hui12581/article/details/44832651 Chap1:JNI完全手册... 3 Chap2:JNI-百度百科... 11 C ...
- View的事件拦截机制浅析
为什么要去分析view的事件 记得上周刚立的flag就是关于view的事件机制.那现在我来说说我对view的感受.关于view的事件,百度google一搜.一批又一批.但是能让人理解的少之又少.换句话 ...
- 浅析JavaScript事件流——冒泡
一.什么是事件冒泡流 我们知道事件流指的是从页面中接受事件的顺序. 为了形象理解事件冒泡,可以想象三军主将诸葛亮,在帐内运筹帷幄,眼观六路耳听八方,这时候前方的战事情况就需要靠传令兵来传达,当第一位传 ...
- [蓝牙] 2、蓝牙BLE协议及架构浅析&&基于广播超时待机说广播事件
第一章 BLE基本概念了解 一.蓝牙4.0和BLE区别 蓝牙4.0是一种应用非常广泛.基于2.4G射频的低功耗无线通讯技术.蓝牙低功耗(Bluetooth Low Energy ),人们又常称之为 ...
- jquery的ready事件的实现机制浅析
页面初始化中,用的较多的就是$(document).ready(function(){//代码}); 或 $(window).load(function(){//代码}); 他们的区别就是,ready ...
随机推荐
- 安装 rbbitMQ redis mongo的三个扩展
#!/bin/bash###install redis extend #########cd /usr/local/srctar fxvz redis-2.2.7.tgzcd redis-2.2.7/ ...
- 在linux下安装配置rabbitMQ详细教程
在linux下安装配置rabbitMQ详细教程 2017年12月20日 17:34:47 阅读数:7539 安装Erlang 由于RabbitMQ依赖Erlang, 所以需要先安装Erlang. Er ...
- ARM与X86架构的对决[整编]
CISC(复杂指令集计算机)和RISC(精简指令集计算机)是当前CPU的两种架构.它们的区别在于不同的CPU设计理念和方法.早期的CPU全部是CISC架构,它的设计目的是 CISC要用最少的机器语言 ...
- Tuning 16 Using Materialized view
物化视图表示在数据库的其他地方另外存放了一份as 后边的内容, 如果只是普通view, 那么 rowid 是相同的, view相当于指针, 它指向基表. 而物化视图的rowid 与基表是不一样的, 所 ...
- sql语句中3表删除和3表查询
好久没来咱们博客园了,主要近期在忙一些七七八八的杂事,包括打羽毛球比赛的准备和自己在学jqgrid的迷茫.先不扯这些没用的了,希望大家能记得小弟,小弟在此谢过大家了. 回归正题:(以下的sql是本人在 ...
- js禁止别人查看源码
1.直接按F12 2.Ctrl+Shift+I查看 3.鼠标点击右键查看 4.Ctrl+u=view-source:+url 把以上三种状态都屏蔽掉就可以了,document有onkeydown(键盘 ...
- js数组的方法
arrayObject.join(separator) 将数组以separator字符为间隔转化为字符串并返回,如果不传,默认为逗号.此方法不会改变原数组 let arr = [1,2,3]; arr ...
- Eclipse设置护眼背景
Window-->Preferences-->General-->Editors-->Text Editors-->Background color 自定义颜色:色调:8 ...
- 【BZOJ3039】玉蟾宫 单调栈
[BZOJ3039]玉蟾宫 Description 有一天,小猫rainbow和freda来到了湘西张家界的天门山玉蟾宫,玉蟾宫宫主蓝兔盛情地款待了它们,并赐予它们一片土地.这片土地被分成N*M个格子 ...
- 【BZOJ3829】[Poi2014]FarmCraft 树形DP(贪心)
[BZOJ3829][Poi2014]FarmCraft Description In a village called Byteville, there are houses connected ...