【C#】详解C#事件
目录结构:
在这篇Blog中,笔者会详细阐述C#中事件的使用。
1.事件基本介绍
C#中定义了事件成员的类型,允许类型通知其它类型发生了特定的事情。事件是基于委托为基础的,说白了就是对委托的封装,委托就是一种回调方法的机制,笔者认为设计事件就是为了能够更好地理解面向对象。
事件(Event) 基本上说是一个用户操作,如按键、点击、鼠标移动等等,或者是一些出现如系统生成的通知。应用程序需要在事件发生时响应事件。例如,中断。事件是用于进程间通信。
为了更好地理解事件,这里笔者描述一个场景:有一个按钮,当双击该按钮的时候,很有可能希望其他的动作也被触发。
如图:
圆圈1,表示第一步:首先把CallBacker1的callback1()方法和CallBacker2的callbacker2()方法注册到Button的DoubleClick事件中。
圆圈2,表示第二步:引发Button的DoubleClick。
圆圈3,表示第三步:触发在注册在Button的DoubleClick事件中的所有回调方法。
下面笔者将会按照上面的情景来讲解C#中事件的知识点。
1.1 定义事件类型
事件引发时,引发事件的对象可能希望向接受事件通知的对象传递一些附加信息。根据约定,这种类应该从System.EventArgs派生,而且类名以EventArgs结束。
这里笔者定义一个NewButtonClickEventArgs类,用来容纳被点击按钮的文本信息,
class NewButtonClickEventArgs : EventArgs{
private readonly String text;
public NewButtonClickEventArgs(String text) {
this.text = text;
}
public String Text { get { return text; } }
}
EventArgs类在Microsoft .NET Framework中定义,EventArgs是一个基类型。
EventArgs的源码如下:
[Serializable]
[System.Runtime.InteropServices.ComVisible(true)]
public class EventArgs {
public static readonly EventArgs Empty = new EventArgs(); public EventArgs()
{
}
}
可以看出EventArgs的类型非常简单,不会附加任何传递信息,主要目的是作为其他类型的基类。当然,如果时间不需要传递任何附加信息,那么就可以用该类。
1.2 定义事件成员
在C#中定义事件成员使用event关键字,每个事件成员几乎都会指定以下信息:
a.可访问性标识符。
b.委托类型,以及需要委托的原型。
c.事件名称
例如:
sealed class Button {
//定义事件成员
public event EventHandler<NewButtonClickEventArgs> DoubleClick;
...
}
我们指定了EventHanler泛型委托,该委托的元数据如下:
public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e)
1.3 定义引发事件的方法
按照约定,类要定义一个受保护的虚方法,但是如果该类是密封的,那么该方法就应该声明为私有的和非虚的。
sealed class Button {
...
//定义引发事件的方法
private void OnDoubleClick(NewButtonClickEventArgs e) {
EventHandler<NewButtonClickEventArgs> temp = Volatile.Read(ref DoubleClick);
if (temp != null) {
temp(this,e);
}
}
...
}
1.3.1 以线程安全的方式引发事件
在上面定义引发事件的方法中,我们使用了如下的代码:
EventHandler<NewButtonClickEventArgs> temp = Volatile.Read(ref DoubleClick);
相信在事件的调用中,经常都会看到如上形式的代码,接下来笔者将会讲解原因:
在.net Framework刚发布的时建议开发者使用如下的形式引发事件:
if(DoubleClick!=null){
DoubleClick(this,e);
}
这样做的问题是,虽然当前线程检查出了DoubleClick不为空,但有可能存在如下情况,当前线程在检查了DoubleClick不为空后,在还没调用DoubleClick之前,其它线程修改了DoubleClick,比如移除了委托链上的所有方法,那么当前线程再次调用DoubleClick的时候,就有可能NullReferenceException。
这是一个竞态问题,我们可以修改如下形式:
EventHandler<NewButtonClickEventArgs> temp=DoubleClick;
if(temp!=null){
temp(this,e);
}
它的思路是,把DoubleClick复制到临时变量temp中,这样的话,即使其他线程改变了DoubleClick事件,那么也不会出错。
但是如果编译器擅自做主,进行优化,移除临时变量temp,那么上面的方式就和第一种方式就没什么区别,仍然有可能抛出NullReferenceException异常。然而,编译器是理解这种这种模式的,不会把temp优化掉优化,所以这是一种安全的方式。
上面之所以能够安全调用,是因为编译器能够“理解”正确,一般情况下,我们是不太知道编译器是如何理解的,所以能够强制提醒一下编译器就更好了,如下的方法:
EventHandler<NewButtonClickEventArgs> temp = Volatile.Read(ref DoubleClick);
if (temp != null)
{
temp(this, e);
}
这里使用了Volatile类的Read方法,以线程安全的方式把DoubleClick复制到temp变量中,这样的话,编译器绝不会吧temp变量优化掉。
1.4. 登记事件关注
上面我们已经定义好了事件,接下来就是登记事件关注。
定义如下类:
class CallBacker{
public CallBacker(Button btn){
btn.DoubleClick += CallBack;
}
public void CallBack(Object sender,NewButtonClickEventArgs args){
Console.WriteLine("按钮文本为:"+args.Text);
}
}
在这个类的构造方法中,我们完成对DoubleClick事件的关注。
到这里我们就完成一个简单的事件过程,完整代码如下:
class NewButtonClickEventArgs : EventArgs{
private readonly String text; public NewButtonClickEventArgs(String text) {
this.text = text;
} public String Text { get { return text; } }
}
sealed class Button {
//定义事件成员
public event EventHandler<NewButtonClickEventArgs> DoubleClick; // 定义引发事件的方法
public void OnDoubleClick(NewButtonClickEventArgs e) {
EventHandler<NewButtonClickEventArgs> temp = Volatile.Read(ref DoubleClick);
if (temp != null)
{
temp(this, e);
}
}
}
class CallBacker{
public CallBacker(Button btn){
btn.DoubleClick += CallBack;
}
public void CallBack(Object sender,NewButtonClickEventArgs args){
Console.WriteLine("按钮文本为:"+args.Text);
}
}
2 揭秘事件
为了弄清楚事件到底是什么,我们编译如下C#代码:
namespace ConsoleApplication2
{
class Program
{
//定义委托
delegate void MyDelegate(Object obj);
//定义事件
static event MyDelegate MyEvent; static void Main(string[] args)
{
MyEvent += Test1;//注册方法
MyEvent += Test2;//注册方法 MyEvent(new Object());//调用
Console.ReadLine();
}
static void Test1(Object obj) {
Console.WriteLine("test1");
}
static void Test2(Object obj) {
Console.WriteLine("test2");
}
}
}
我们在编译上面的C#代码后,用ildasm工具打开它,可以看到如下这样:
除了所定义的成员,还多了一个类(MyDelegate),一个字段(MyEvent),两个方法(add_MyEvent、remove_MyEvent)。其中类是由委托转化而来,这里不做详细参数,详情可以参见C#详解委托。
一个事件的声明是可以转化为一个代理字段的声明加上添加、删除两种方法的事件操作。上面的MyEvent事件与MyEvent字段、add_MyEvent方法、remove_MyEvent方法关联起来了。
再打开MyEvent事件的IL的IL代码,可以看到出现这样
可以看出,事件的addOn和removeOn分别被重定向到了类中的add_MyEvent和remove_MyEvent方法上。
笔者认为之所以要利用代理字段,原因很有可能是CLS不直接支持事件参与运行,因为说到底,事件还是属于引用类型变量。
3 显式实现事件
3.1 为什么需要显式实现事件
在最开始我们已经知道了事件是基于委托的,也就是说事件是对委托的封装,一个事件的底层肯定有一个委托列表做支撑。
在System.Windows.Forms.Control类型中定义了大约70个事件,
假如Control类型在实现事件时,允许编译器生成add和remove访问器方法以及委托字段(每个事件都生成一个维护委托的委托列表),那么每个Control仅为事件就要多准备70个字段,这是非常浪费内存的。
然而这种情况,是确实存在的。
例如有如下代码:
namespace ConsoleApplication2
{
class Program
{
//定义委托
delegate void MyDelegate(Object obj);
//定义事件
static event MyDelegate MyEvent1;
static event MyDelegate MyEvent2; static void Main(string[] args)
{ }
}
}
编译后,再使用ildasm工具打开,可以看到如下情况:
可以看出,我们定义了两个事件就出现了两个字段和四个方法,和上面对比不难发现,每当多定义一个事件,那么编译器就会为其新创建一个字段和两个方法。可想而知,如果定义70个事件会怎么样。
如果定义一种事件能够被其他事件所共用就好了,接下来将讨论如何实现这个思路。
3.2 显式实现事件的实现
为了高效率的存储委托,公开了事件的每个对象都要维护一个集合(数据字典),集合将某种形式的事件标识符作为键(Key),将委托列表作为值(Value)。新对象构造时,这个集合是空白的。登记对一个事件的关注时,会在集合列表中查找该事件的标识符,如果存在这个标识符,就将新委托对象和旧委托对象合并。如果不存在,那么就添加当前委托对象和标识符到集合列表中。
这样一来,我们就免去了自定义事件的步骤,按照上面的思想,将委托链表和某些键关联起来存储在集合中,当我们需要操作某些委托列表时,直接通过对应的键从集合列表中取出对应的委托链就可以了。这个过程未使用过事件,性能更高效。
//在使用EventSet类时,作为Key使用。
public sealed class EventKey { } public sealed class EventSet {
//该字典用户维护 EventKey -> Delegate 的映射
private readonly Dictionary<EventKey, Delegate> m_events = new Dictionary<EventKey, Delegate>(); //添加 EventKey -> Delegate 的映射(如果不存在)
//将新委托合并到旧委托中去(如果已经存在该EventKey的映射)
public void Add(EventKey eventKey, Delegate handler) {
Monitor.Enter(m_events);
Delegate d;
m_events.TryGetValue(eventKey,out d);
m_events[eventKey] = Delegate.Combine(d,handler);
Monitor.Exit(m_events);
} //从eventKey映射的Delegate中删除hanlder委托
//在删除最后一个委托后,同时删除 eventKey -> Delegate的映射
public void remove(EventKey eventKey, Delegate handler) {
Monitor.Enter(m_events);
Delegate d;
if (m_events.TryGetValue(eventKey, out d)) {
d = Delegate.Remove(d,handler); if (d == null) { //没有委托了
m_events.Remove(eventKey);
}
}
Monitor.Exit(m_events);
} //为指定eventKey映射的委托触发
public void Raise(EventKey eventKey,Object sender,EventArgs e) {
Monitor.Enter(m_events);
Delegate d;
m_events.TryGetValue(eventKey,out d);
Monitor.Exit(m_events);
if (d != null) {
//以对象数组的形式传递参数,如果参数不匹配DynamicInvoke会抛出异常。
d.DynamicInvoke(sender,e);
}
}
}
上面定义了两个类EventKey和EventSet,其中EventKey是用于维护EventSet的私有数据字典的(利用EventKey对象的Hash值),EventSet中定义了三个方法Add,Remove,Raise,这三个方法都利用Monitor类的同步访问对象机制来操作字典表。
在定义好维护委托列表的类后,我们就可以按照如下的栗子来使用了:
class FooEventArgs : EventArgs { }
class TypeWithLotsOfEvents { private readonly EventSet m_eventSet = new EventSet();
protected static readonly EventKey s_fooEventKey = new EventKey(); //使派生类也能够访问
protected EventSet EventSet
{
get{return m_eventSet;}
} //定义事件访问器
public event EventHandler<FooEventArgs> Foo {
add { m_eventSet.Add(s_fooEventKey,value); }
remove { m_eventSet.remove(s_fooEventKey, value); }
} //定义触发事件的受保护的虚方法
protected virtual void OnFoo(FooEventArgs e) {
m_eventSet.Raise(s_fooEventKey,this,e);
} //定义将输入转化为这个事件的方法
public void SimulateFoo() {
OnFoo(new FooEventArgs());
}
}
调用代码:
public sealed class Program {
static void Main(String[] args) {
TypeWithLotsOfEvents typeWithLotsOfEvents = new TypeWithLotsOfEvents();
typeWithLotsOfEvents.Foo += HandlerFooEvent; typeWithLotsOfEvents.SimulateFoo();
}
static void HandlerFooEvent(Object obj, FooEventArgs e) {
Console.WriteLine("here arrived ...");
}
}
【C#】详解C#事件的更多相关文章
- JAVASCRIPT事件详解-------原生事件基础....
javaScirpt事件详解-原生事件基础(一) 事件 JavaScript与HTML之间的交互是通过事件实现的.事件,就是文档或浏览器窗口中发生的一些特定的交互瞬间,通过监听特定事件的发生,你能 ...
- 百度地图API详解之事件机制,function“闭包”解决for循环和监听器冲突的问题:
原文:百度地图API详解之事件机制,function"闭包"解决for循环和监听器冲突的问题: 百度地图API详解之事件机制 2011年07月26日 星期二 下午 04:06 和D ...
- 详解键盘事件(keydown,keypress,keyup)
一.键盘事件基础 1.定义 keydown:按下键盘键 keypress:紧接着keydown事件触发(只有按下字符键时触发) keyup:释放键盘键 顺序为:keydown -> keypre ...
- 详解 Solidity 事件Event - 完全搞懂事件的使用
很多同学对Solidity 中的Event有疑问,这篇文章就来详细的看看Solidity 中Event到底有什么用? 写在前面 Solidity 是以太坊智能合约编程语言,阅读本文前,你应该对以太坊. ...
- Android: 详解触摸事件如何传递
当视图的层次结构比较复杂的时候,触摸事件的响应流程也变得复杂. 举例来说,你也许有一天想要制作一个手势极其复杂的 Activity 来折磨你的用户,你经过简单思索,认为其中应该包含一个 PageVie ...
- javaScirpt事件详解-原生事件基础(一)
事件 JavaScript与HTML之间的交互是通过事件实现的.事件,就是文档或浏览器窗口中发生的一些特定的交互瞬间,通过监听特定事件的发生,你能响应相关的操作.图片引用:UI Events 事件流 ...
- Android事件详解——拖放事件DragEvent
1.Android拖放框架的作用? 利用Android的拖放框架,可以让用户用拖放手势把一个View中的数据移到当前layout内的另一个View中去. 2.拖放框架的内容? 1)拖放事件类 2)拖放 ...
- 详解javascript事件绑定使用方法
由于html是从上至下加载的,通常我们如果在head部分引入javascript文件,那么我们都会在javascript的开头添加window.onload事件,防止在文档问加载完成时进行DOM操作所 ...
- js this详解,事件的三种绑定方式
this,当前触发事件的标签 在绑定事件中的三种用法: a. 直接HTML中的标签里绑定 onclick="fun1()"; b. 先获取Dom对象,然后利用dom对象在js里绑定 ...
随机推荐
- eclipse错误GC overhead limit exceeded
1.eclipse以外关闭后打开错误如下图: 2.具体详情: 3.An internal error occurred during: "Building workspace". ...
- BZOJ1801 [Ahoi2009]chess 中国象棋 动态规划
欢迎访问~原文出处——博客园-zhouzhendong 去博客园看该题解 题目传送门 - BZOJ1801 题意概括 在N行M列的棋盘上,放若干个炮可以是0个,使得没有任何一个炮可以攻击另一个炮. 请 ...
- Strom在本地运行调试出现的错误
1.错误日志 31385 [main] WARN backtype.storm.daemon.nimbus - Topology submission exception. (topology nam ...
- 求链表的倒数第m个元素
法一: 首先遍历一遍单链表,求出整个单链表的长度n,然后将倒数第m个,转换为正数第n-m+1个,接下去遍历一次就可以得到结果. 不过这种方法需要对链表进行两次遍历,第一次遍历用于求解单链表的长度,第二 ...
- [OpenCV-Python] OpenCV 中图像特征提取与描述 部分 V (二)
部分 V图像特征提取与描述 OpenCV-Python 中文教程(搬运)目录 34 角点检测的 FAST 算法 目标 • 理解 FAST 算法的基础 • 使用 OpenCV 中的 FAST 算法相关函 ...
- moodleform -转载于blfshiye
Form API 表单API 文件夹 1.概述 2.亮点 3.使用方法 4.表单元素 4.1 基本表单元素 4.2 定制表单元素 5.经常使用函数 5.1 add_action_buttons($c ...
- P1939【模板】矩阵加速(数列)
P1939[模板]矩阵加速(数列)难受就难受在a[i-3],这样的话让k=3就好了. #include<iostream> #include<cstdio> #include& ...
- 机器学习 支持向量机(SVM) 从理论到放弃,从代码到理解
基本概念 支持向量机(support vector machines,SVM)是一种二分类模型,它的基本模型是定义在特征空间上的间隔最大的线性分类器.支持向量机还包括核技巧,这使它成为实质上的非线性分 ...
- 网络爬虫之scrapy爬取某招聘网手机APP发布信息
1 引言 过段时间要开始找新工作了,爬取一些岗位信息来分析一下吧.目前主流的招聘网站包括前程无忧.智联.BOSS直聘.拉勾等等.有段时间时间没爬取手机APP了,这次写一个爬虫爬取前程无忧手机APP岗位 ...
- JS中获取文件点之后的后缀字符
var FileName = $("#file").val(); var index1=FileName.lastIndexOf("."); var index ...