[CLR via C#]11. 事件
一、 设计要公开事件的类型
如果类型定义了事件成员,那么类型(或类型实例)就可以通知其他对象发生了一些特定的事情。
例如,Button类提供了一个名为Click的事件。应用程序中的一个或多个对象可能想接受这个事件的通知,以便在Button被单击之后采取某些操作。事件就是实现这种交互的类型成员。


/// <summary>
/// 第一步:定义类型来容纳所有需要发送给事件接收者的附加信息
/// </summary>
internal sealed class NewMailEventArgs : EventArgs
{ private readonly String m_from, m_to, m_subject; public NewMailEventArgs(String from, String to, String subject)
{
m_from = from; m_to = to; m_subject = subject;
}
/// <summary>
/// 邮件发件人
/// </summary>
public String From { get { return m_from; } }
/// <summary>
/// 邮件收件人
/// </summary>
public String To { get { return m_to; } }
/// <summary>
/// 邮件主题
/// </summary>
public String Subject { get { return m_subject; } }
} //后续的步骤在MailManager类中进行
internal class MailManager {
}
第二步 :定义事件成员
internal class MailManager {
// 第二步:定义事件成员
public event EventHandler<NewMailEventArgs> NewMail;
......
}
public delegate void Eventhandler<TEventArgs>(Object sender, TEventArgs e) where TEventArgs: EventArgs;
所以方法原型必须具有以下形式:
void MethodName(Object sender, TEventArgs e);
internal class MailManager
{
......
/// <summary>
/// 第三步:定义负责引发事件的方法来 通知事件的登记对象
/// 如果类是密封的,这个方法要声明为私有和非虚
/// </summary>
/// <param name="e"></param>
protected virtual void OnNewMail(NewMailEventArgs e)
{
// 处于线程安全考虑,现在将对委托字段的引用复制到一个临时字段中
EventHandler<NewMailEventArgs> temp = NewMail; // 任何方法登记了对事件的关注,就通知它们
if (temp != null)
{
temp(this, e);
}
}
.....
}
internal class MailManager {
......
// 第四步:定义方法将输入转换为期望事件
public void SimulateNewMail(String from, String to, String subject) {
// 构造一个对象来容纳想传给通知接收者的消息
NewMailEventArgs e = new NewMailEventArgs(from, to, subject);
// 调用虚方法通知对象事件已发生
OnNewMail(e);
}
......
}
二、 编译器如何实现事件
我们仔细扒一扒事件是什么?它是如何工作的。
public event EventHandler<NewMailEventArgs> NewMail;
我们来看看发生了什么?
// 1. 一个被初始化为null的私有委托字段
private EventHandler<NewMailEventArgs> NewMail= null; //2. 一个公共add_Xxx方法(其中Xxx是事件名)
//允许方法登记对事件的关注
public void add_NewMail(EventHandler<NewMailEventArgs> value)
{
//通过循环对CompareExchange的调用
//可以以一种线程安全的方式向事件添加委托
EventHandler<NewMailEventArgs> handler2;
EventHandler<NewMailEventArgs> newMail = this.NewMail;
do
{
handler2 = newMail;
EventHandler<NewMailEventArgs> handler3 = (EventHandler<NewMailEventArgs>) Delegate.Combine(handler2, value);
newMail = Interlocked.CompareExchange<EventHandler<NewMailEventArgs>>(ref this.NewMail, handler3, handler2);
}
while (newMail != handler2);
} //3. 一个公共remove_Xxx方法(其中Xxx是事件名)
//允许方法注销对事件的关注
public void remove_NewMail(EventHandler<NewMailEventArgs> value)
{
//通过循环对CompareExchange的调用
//可以以一种线程安全的方式向事件移除委托
EventHandler<NewMailEventArgs> handler2;
EventHandler<NewMailEventArgs> newMail = this.NewMail;
do
{
handler2 = newMail;
EventHandler<NewMailEventArgs> handler3 = (EventHandler<NewMailEventArgs>) Delegate.Remove(handler2, value);
newMail = Interlocked.CompareExchange<EventHandler<NewMailEventArgs>>(ref this.NewMail, handler3, handler2);
}
while (newMail != handler2);
}
第一个构造是具有恰当委托类型的字段。该字段是对一个委托列表的头部的引用。事件发生时会通知这个列表中的委托。字段初始化为null,表示无监听者登记对该事件的关注。
internal sealed class Fax
{
/// <summary>
/// 将MailManager对象传给构造器
/// </summary>
/// <param name="mm"></param>
public Fax(MailManager mm)
{
// 构造EventHandler<NewMailEventArgs>委托的一个实例,
// 使它引用我们的FaxMsg回调方法
// 向MailManager的NewMail时间等级我们的回调方法
mm.NewMail += FaxMsg;
} /// <summary>
/// 新邮件到达时,MailManager将调用这个方法
/// </summary>
/// <param name="sender">MailManager对象,便于回调</param>
/// <param name="e">NewMailEventArgs对象</param>
private void FaxMsg(Object sender, NewMailEventArgs e)
{
Console.WriteLine("Faxing mail message:");
Console.WriteLine(" From={0}, To={1}, Subject={2}",
e.From, e.To, e.Subject);
} // 执行这个方法,Fax对象指向NewMail事件注销自己对它的关注,以后便不会接受通知
public void Unregister(MailManager mm)
{
// 向MailManagerde的NewMail事件注销自己对这个事件的关注
mm.NewMail -= FaxMsg;
}
}
在Fax构造器中,Fax对象使用C#的+=操作符登记它对MailManager的NewMail事件的关注。
mm.NewMail += FaxMsg;
因为C#编译器内建对事件的支持,所以会将+=操作符翻译成为以下代码来添加对事件的关注:
mm.add_NewMail (new EventHandler<NewMailEventArgs>(this.FaxMsg));
即使使用的编程语言不直接支持事件,也可以显示调用add访问器方法向事件登记一个委托。两者效果是相同的,后者只是源代码看起来没那么优美而已。两者最终都是用add访问器将委托添加到事件的委托列表中,从而完成委托向事件的登记。
在Unregister方法中,我们使用了"-="来向MailManagerde的NewMail事件注销自己对这个事件的关注,当C#编译器看到使用"-="操作符时向事件注销一个委托时,会生成对事件的remove方法的调用:
mm.remove_NewMail (new EventHandler<NewMailEventArgs>(this.FaxMsg));
和"+="一样,即使使用的编程语言不直接支持事件,也可以显示调用remove访问器方法向事件注销一个委托。remove方法为了向事件注销委托,需要扫描委托列表来寻找恰当的委托。如发现一个匹配,该委托会从事件的委托列表中删除。如果没发现匹配,那么不会报错,列表不会发生变化。
C#要求代码使用+=和-=操作符在列表中增删委托。
四、显式实现事件
System.Windows.Forms.Control类型定义了约70个事件。假如Control类型在实现事件时,是允许编译器隐式生成add和remove访问器方法以及委托字段,那么每个Control对象都会包含70个委托字段。由于大多数开发人员只关心少数几个事件,所以从Control派生类型创建的对象都会浪费大量内存。
目前。讨论C#编译器如何允许类的开发人员显式实现一个事件,使开发人员能够控制add和remove方法来操纵回调委托的方式。这里演示如何通过显式实现事件来高效率地实现一个提供大量事件的类。
为了高效率存储事件委托,公开了事件每个对象都要维护的一个集合(通常是一个字典)。这个集合将某种形式的事件标识作为键(key)。新对象构造时,这个集合是空白的。登记对一个事件的关注时,会在集合中查找事件的标识符。如果事件标识符已在其中,新委托就和这个事件的委托列表合并。如果事件标识符不再集合中,就添加事件标识符和委托。
对象需要引发一个事件时,会在集合中查找事件标识符。如果集合中没有找到事件标识符,表明还没有任何对象登记对这个事件的关注,所以没有任何委托需要回调。如果事件标识符在集合中,就调用它关联的委托列表。这个设计模式的实现是定义了事件那个类型的开发人员的责任;使用类型的开发人员不知道事件在内部是如何实现的。
下面展示了如何完成这个模式的。首先实现的是一个EventSet类,它代表一个集合,其中包含了事件以及每个事件的委托列表。
/// <summary>
/// 这个类目的是在使用EventSet时,提供多一点的类型安全性和代码可维护性
/// </summary>
public sealed class EventKey : Object
{
} /// <summary>
/// 代表一个集合,其中包括了事件以及事件的委托列表
/// </summary>
public sealed class EventSet
{
/// <summary>
/// 私有字典,用于维护EventKey -> Delagate 映射
/// </summary>
private readonly Dictionary<EventKey, Delegate> m_events =
new Dictionary<EventKey, Delegate>(); /// <summary>
/// 添加一个 EventKey -> Delegate 映射 (如果EventKey不存在)
/// 或者将一个委托与一个现有的EventKey合并
/// </summary>
/// <param name="eventKey"></param>
/// <param name="handler"></param>
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);
} /// <summary>
/// 从EventKey(如果存在)删除一个委托,并且
/// 再删除最后一个委托时删除EventKey -> Delegate 映射
/// </summary>
/// <param name="eventKey"></param>
/// <param name="handler"></param>
public void Remove(EventKey eventKey, Delegate handler)
{
Monitor.Enter(m_events);
// 调用TryGetValue,确保在尝试从集合中删除一个不存在的EvenrKey时,
// 不会抛出异常
Delegate d;
if (m_events.TryGetValue(eventKey, out d))
{
d = Delegate.Remove(d, handler); // 如果还有委托,就设置新的头部(地址),否则删除EventKey
if (d != null) m_events[eventKey] = d;
else m_events.Remove(eventKey);
}
Monitor.Exit(m_events);
} /// <summary>
/// 为指定的EventKey -> Delegate 映射引发事件
/// </summary>
/// <param name="eventKey"></param>
/// <param name="sender"></param>
/// <param name="e"></param>
public void Raise(EventKey eventKey, Object sender, EventArgs e)
{
// 如果EventKey不在集合中,不抛出异常
Delegate d;
Monitor.Enter(m_events);
m_events.TryGetValue(eventKey, out d);
Monitor.Exit(m_events); if (d != null)
{
// 由于字典可能包含几个不同的委托类
// 所以无法在编译时构造一个类型安全的委托调用
// 因此,我调用System.Delegate类型的DynamicInvoke方法,
// 以一个对象数组的形式向它传递回调方法的参数。
// 在内部,DynamicInvoke会向调用的回调方法查证参数的
// 类型安全性,并调用方法。
// 如果存在类型不匹配情况,则抛出异常
d.DynamicInvoke(new Object[] { sender, e });
}
}
}
接着,让我们定义一个使用EventSet类。在这个类中,一个字段引用了一个EventSet对象,而且这个类的每一个事件都是显示实现的,使用每个事件的add方法都将制定的回调委托存储到EventSet对象中,而且每个事件的remove方法都删除指定的回调委托。
using System; // 为这个事件定义从EventArgs派生的类型
public class FooEventArgs : EventArgs { } internal class TypeWithLotsOfEvents { // 定义一个虚实例字段,它引用一个集合
// 集合用来管理一组"事件/委托"对
// 注意: EnentSet类型不是FCl一部分,它是我们自己定义的类型
private readonly EventSet m_eventSet = new EventSet(); // protected属性使派生类型能访问集合
protected EventSet EventSet { get { return m_eventSet; } } #region 用于支持Foo事件的代码 (为附加的事件重复这个模式)
// 定义Foo事件必要的成员
// 2a. 构造一个静态只读对象来标识这个事件
// 每个对象都有它自己的哈希码,以便在对象的集合中查找这个事件的委托链表
protected static readonly EventKey s_fooEventKey = new EventKey(); // 2d. 定义事件的访问器方法,用于在集合中增删委托
public event EventHandler<FooEventArgs> Foo {
add { m_eventSet.Add(s_fooEventKey, value); }
remove { m_eventSet.Remove(s_fooEventKey, value); }
} // 2e. 为这个事件定义受保护的虚方法OnFoo
protected virtual void OnFoo(FooEventArgs e) {
m_eventSet.Raise(s_fooEventKey, this, e);
} // 2f. 定义将输入转换成为这个事件的方法
public void SimulateFoo() {
OnFoo(new FooEventArgs());
}
#endregion
}
使用TypeWithLotsOfEvents类型的代码不知道事件是由编译器隐式实现的,还是由开发人员显式实现的。它们只需要标准语法向事件登记即可:
public static void Main() {
TypeWithLotsOfEvents twle = new TypeWithLotsOfEvents();
// 添加一个回调
twle.Foo += HandleFooEvent;
twle.SimulateFoo();
}
private static void HandleFooEvent(object sender, FooEventArgs e)
{
Console.WriteLine("Handling Foo Event here...");
}
[CLR via C#]11. 事件的更多相关文章
- 【CLR in c#】事件
1.事件模型建立在委托的基础上. 2,定义事件编译器会做三个动作,第一个构造具有委托类型的字段,事件发生时会通知这个列表中的委托. 第二个构造的是一个添加关注的方法+=. 第三个构造是一个注销事件关 ...
- 【C#进阶系列】11 事件
事件,定义了事件成员的类型允许类型或类型的实例通知其它对象发生了特定的事情. 按照我自己的理解而言,事件可以被(方法)关注,也可以被(方法)取消关注,事件发生后关注了事件的一方会了解到,并对事件做出相 ...
- cocos基础教程(11)事件分发机制
cocos3.0的事件分发机制: 创建一个事件监听器-用来实现各种触发后的逻辑. 事件监听器添加到事件分发器_eventDispatcher,所有事件监听器有这个分发器统一管理. 事件监听器有以下几种 ...
- 【CLR VIA C#】读书笔记
工作几年了才看,记录下笔记备忘. 章节 笔记 1.CLR的执行模型 公共语言运行时(Common Language Runtime,CLR) 源代码-->编译器检查语法和分析源代码-->托 ...
- WPF 学习笔记 路由事件
1. 可传递的消息: WPF的UI是由布局组建和控件构成的树形结构,当这棵树上的某个节点激发出某个事件时,程序员可以选择以传统的直接事件模式让响应者来响应之,也可以让这个事件在UI组件树沿着一定的方向 ...
- C#进阶系列 ---- 《CLR via C#》
[C#进阶系列]30 学习总结 [C#进阶系列]29 混合线程同步构造 [C#进阶系列]28 基元线程同步构造 [C#进阶系列]27 I/O限制的异步操作 [C#进阶系列]26 计算限制的异步操作 ...
- JavaScript--DOM事件(笔记)
第1章 事件流1-1.事件冒泡:事件最开始由最具体的元素(文档中嵌套层次最深的那个节点)接收; 然后逐级向上传播至最不具体的那个节点(文档);1-2.事件捕获:不太具体的节点应该更早接收到事件,而最具 ...
- [iOS UI进阶 - 3.0] 触摸事件的基本处理
A.需要掌握和练习的 1.介绍事件类型2.通过按钮的事件处理引出view的事件处理3.响应者对象 --> UIResponder --> UIView4.view的拖拽* 实现触摸方法,打 ...
- NEsper使用的事件类型 z
NEsper使用的事件类型来描述事件的类型信息.你的应用在启动时可能预先配置定义事件类型,或者在运行时通过API或EPL语法动态的增加事件类型. EPL中的create schema 的语法允许在运行 ...
随机推荐
- FoLlow 的技术博客
酷壳 http://coolshell.cn 老赵点滴- 追求编程之美 http://blog.zhaojie.me/ Pixel-In-Gene Blog
- Java 中类的加载顺序
如果类A和类B中有静态变量,静态语句块,非静态变量,非静态语句块,构造函数,静态方法,非静态方法,同时类A继承类B,请问当实例化A时,类内部的加载顺序是什么? 测试代码如下: Class B: pub ...
- HTML Hyperlink between and within pages
In HTML, we can use tag <a href=""> to create hyperlinks between and within pages. T ...
- iOS开发——项目实战总结&带你看看Objective-C的精髓
带你看看Objective-C的精髓 1:接口与实现 @interface...@end @implementation...@end @class 接口(头文件) 实现文件 向前引用 注:类别通过增 ...
- 【cocos2d-x 手游研发----目录】
感谢大家一直支持我写这样一系列的博客,从中我自己也获益良多,cocos2d-x这样一款非常棒的引擎,是值得我们去学习和分享的,谈到分享,那我就把这套写了差不多一两个月的框架给大家开源下载,写的很一般, ...
- thinking in object pool
1.背景 对象池为了避免频繁创建耗时或耗资源的大对象,事先在对象池中创建好一定数量的大对象,然后尽量复用对象池中的对象,用户用完大对象之后放回对象池. 2.问题 目前纵观主流语言的实现方式无外乎3个步 ...
- .NET Framework 类库
.NET Framework 类库 MSDN == V2.0 == .NET Framework 类库是一个由 Microsoft .NET Framework SDK 中包含的类.接口和值类型组成的 ...
- 关于cnas培训
关于cnas(2014-12-9----2014-12-12) 目的:完成内审相关知识培训,可以做一个合格的内审人员 过程: 1.为什么会存在实验室 2.iso9000是怎么产生的,以及存在的意义 3 ...
- mssql表名列名对应语句
if exists (select * from tempdb..sysobjects where name like '#magic%') drop table #magic go select a ...
- 用qt代码怎样编写图片保存格式[qt4.6]
用qt代码怎样编写图片保存格式 qt提供了多个保存图片的接口,比较常用的接口如下 bool QPixmap::save ( const QString & fileName, const ch ...