事件
定义了事件成员
的类型允许 类型或类型实例 通知其他对象发生了特定事情.
例如: Button按钮类提供了Click事件.应用程序中的其他对象可以接收关于该事件的通知,以便在Button被点击之后采取特定的操作.
定义了事件成员的类型
能提供一下功能:
- 方法能登记它对事件的关注.
- 方法能注销它对时间的关注.
- 事件发生时,登记了的方法将收到通知.
类型
之所以能提供事件通知功能,是因为类型维护了一个已登记方法的列表
.事件发生后通知列表中的所有已登记方法.
CLR事件模型是以委托
为基础. 委托是调用回调方法
的一种类型安全的方式.对象凭借回调方法接收它们订阅的通知.
我们可以通过为事件定义事件访问器,来控制事件运算符+=、-=运算符的行为,有两个访问器:add和remove,声明事件的访问器看上去和声明一个属性差不多.
public event EventHandler Elapsed
{
add
{
//... 执行+=运算符的代码
}
remove
{
//... 执行-=运算符的代码
}
}
举例用到事件的场景
设计电子邮件应用程序:
功能:电子邮件到达时,将该邮件转发给传真机,将该邮件转给寻呼机.
0) 先构建一个MailManager的一个实例. MailManager提供了NewMail事件.
1) 构造
Fax和Pager对象时,它们向MailManager的NewMail事件登记
它们自己的一个实例方法
.
2) MailManager收到新邮件时,会引发NewMail事件,使所有已登记的方法都能用自己的方式处理邮件.
第一步:定义类型来容纳所有需要发送给事件通知接收者的附加信息
事件引发时,引发事件的对象可能希望向接收事件通知的对象传递一些附加信息
,这些附加信息要封装到它自己的类中, 这种类应该从EventArgs
派生,并且类名以EventArgs结尾.
public class NewMaillEventArgs : EventArgs
{
// 私有只读字段
// 发件人
private readonly string _m_from;
// 收件人
private readonly string _m_to;
// 主题
private readonly string _m_subject;
public NewMaillEventArgs(string from,string to,string subject)
{
_m_from = from;
_m_to = to;
_m_subject = subject;
}
// 公开的只读属性(只有get访问器)
public string From { get { return _m_from; } }
public string To { get { return _m_to; } }
public string Subject { get { return _m_subject; } }
}
EventArgs类的定义实现
实现非常简单,就是一个让其他类型继承的基类型.
如果事件不需要传递附加信息,则可以直接使用EventArgs.Empty,不用构造新的EventArgs对象. 例如:Button点击事件,调用回调方法就可以了,不需要传递附加信息.
[Serializable]
public class EventArgs
{
public static readonly EventArgs Empty = new EventArgs();
}
第二步:定义事件成员
事件成员使用C#关键字event
来定义。
每个事件成员都要指定以下内容:
- 一个可访问性标识符(几乎肯定是Public,这样其他代码才能访问该事件成员);
- 另一个委托类型,它指出要
调用的方法的原型
,以及一个名称(可以是任意有效的标识符)。
//定义成员变量 NewMaill是事件名称
public event EventHandler<NewMaillEventArgs> NewMaill;
事件成员类型是EventHandler<NewMaillEventArgs>
, 意味着所有接收者都必须提供一个原型和EventHandler<NewMaillEventArgs>
匹配的回调方法
.
由于泛型System.EventHandler
的委托定义如下:
public delegate void EventHandler<TEventArgs>(Object sender, TEventArgs e);
所以方法的原型必须具有以下形式:
void MethodName(Object sender, NewMaillEventArgs e);
事件模式要求所有事件处理程序(方法)的返回类型都是void;
因为引发事件后可能要调用好几个回调方法,但是没办法获得所有方法的返回值,将返回类型void,就不允许回调方法返回值.
但是ResloveEventHandle事件没有遵循这个要求,返回了Assembly类型的对象.
为什么要求sender参数是Object类型
只有MailManager对象
才会引发传递NewMaillEventArgs对象
的事件,似乎以下回调方法更适合原型:
// Object sender 改为 MailManager sender
void MethodName(MailManager sender, NewMaillEventArgs e);
原因是:
- 要求Object主要是因为继承.
假设: MailManager成为SmtpMailManager的基类SmtpMailManager : MailManager
,SmtpMailManager
从基类继承了事件NewMail
, 那参数类型需要由MailManager
转换为SmtpMailManager
,反正都是需要转换的,不如定义成Object
.
- 另一个原因是灵活性,使委托能由多个类型使用.
只要类型
提供一个会传递NewMaillEventArgs对象
的事件,即使PopMailManager
类不从MailManager
类派生,也能使用这个委托.
第三步:定义负责引发事件的方法来通知事件的登记对象
事件主要是在单线程情形中使用,所以线程安全不是问题.
//如果类是密封的,该方法要声明为私有和非虚
protected virtual void OnNewMaill(NewMaillEventArgs e)
{
//出于线程安全的考虑,现在将对委托字段的引用复制到一个临时变量时
// EventHandler<NewMaillEventArgs> temp = NewMaill; 也是可以的
// 事件主要是在单线程的情形中使用,所以线程安全不是问题
// (NewMaill != null) 在多线程中会出现竞态问题
// 以下写法是技术正确的版本.
EventHandler<NewMaillEventArgs> temp = Volatile.Read(ref NewMaill);
//任何方法登记了事件的关注,就通知它们
if (temp != null)
{
temp(this, e);
}
}
(NewMaill != null) 在多线程中会出现竞态问题,调用NewMaill之前,在另外一个线程中可能移除一个委托,使NewMaill成了null. 许多开发者就像EventHandler
temp = NewMaill;这样写, 将NewMail引用复制到变量中,引用的是赋值时发生的委托链. 委托是不可变的.
将MailManager作为基类,派生类可以自由重写OnNewMaill方法,一般情况下,派生类会调用基类的OnNewMaill方法,使登记的方法都能收到通知,但是派生类也可以不允许事件转发.
定义如下扩展方法来封装这个线程安全逻辑
public static class EventArgExtensions
{
public static void Raise<TEventargs>(this TEventargs e, Object sender, ref EventHandler<TEventargs> eventHandler)
{
EventHandler<TEventargs> temp = Volatile.Read(ref eventHandler);
if (temp != null)
{
temp(sender, e);
}
}
}
为了方便起见,就可以如下重写OnNewMail方法
protected virtual void OnNewMaill(NewMaillEventArgs e)
{
e.Raise(this, ref m_NewMail);
}
第四步:定义方法将输入转化为期望事件
public void SimulateNewMaill(String from, String to, String subject)
{
// 构造对象来容纳想附加给接收者的信息
NewMaillEventArgs e =new NewMaillEventArgs(from,to,subject);
// 调用虚方法通知事件已经发生
// 如果没有类型重写该方法,我们的对象将通知事件的所有登记对象
OnNewMaill(e);
}
事件是什么? 是如何工作的?
// 事件成员
public EventHandler<NewMaillEventArgs> NewMail = null;
C#编译器编译时把它转换为3个构造:
- 一个被初始化为null的
私有委托字段
;- 是对委托列表头部的引用.
- 一个公共的
add_Xxx方法
(Xxx是事件名,例如add_NewMail)- 生成的代码总是调用
System.Delgate的静态Combine方法
,将委托实例添加到委托列表
中,返回新的列表头地址
, 将这个地址存回字段.
- 生成的代码总是调用
- 一个公共的
remove_Xxx
(Xxx是事件名,例如remove_NewMail)- 生成的代码总是调用
System.Delgate的静态Remove方法
,将委托实例从委托列表中删除
中,返回新的列表头地址
, 将这个地址存回字段.
- 生成的代码总是调用
如果试图删除从未添加过的方法, Delgate
内部不做任何事情,也不会抛出异常, 事件的集合保持不变.
add和remove以线程安全的一种模式更新值.
事件的可访问性决定了什么代码能登记和注销对事件的关注.无论如何只有类本身可以访问委托字段.
设计侦听事件的类型
- 在Fax类中的构造函数传入MailManager,并将对该对象的引用保存到变量中,
- 在构造函数中登记它对MailManager的事件的关注
mm.NewMail += FaxMsg;
- C#编译器对
+=
操作符翻译成以下代码来添加对象对事件的关注.mm.add_NewMail(new EventHandler<NewMaillEventArgs>(this.Msg));
- C#编译器调用了
MailManager
类的add_NewMail
方法,传递新的委托对象(Msg).
- MailManager对象引发事件时, Fax对象的FaxMsg方法会被调用.
- 调用时,传递的第一个参数时MailManager对象引用,
sender
. 可以用来访问MailManager对象的成员. - NewMaillEventArgs附带了信息.
- 调用时,传递的第一个参数时MailManager对象引用,
- 类型实现了IDisposable的Dispose方法,就应该在其中注销对所有事件的关注. 用
-=
操作符(会调用remove方法).
C#要求代码使用+=和-=操作符在列表中增删委托.
什么情况需要显式实现事件?
在一个类型中定义了很多事件,也就是要很多个委托字段,但是用到的只是少数事件, 这样从这个类派生创建的对象都要浪费大量内存.
因此需要开发人员显式实现一个事件,使开发人员能够控制add和remove方法
处理回调委托.
- 高效率存储事件委托, 公开了事件的每个对象都要维护一个集合(通常是字典)
事件标识符作
为key,委托列表
作为值value- 新对象构造时, 集合是空白的, 登记事件时会查找集合中的键(事件标识符),如果已经有了,就将新委托和委托列表合并. 不存在就新添加.
- 所以这个设计,需要定义事件的那个类型开发人员去完成.
EventKey和EventSet设计
using System;
using System.Collections.Generic;
using System.Threading;
// 此类目的:提供多一点的类型安全性和代码的可维护性
// 用于 字典的key键存放每个事件标识符类的哈希码,以便查找这个事件的委托链表
public sealed class EventKey : Object
{
}
// 用于显示实现事件的字典集合
public sealed class EventSet
{
// 定义私有只读字典,用于维护EventKey->Delegate(委托链表)映射
private readonly Dictionary<EventKey, Delegate> m_events =
new Dictionary<EventKey, Delegate>();
// Adds an EventKey -> Delegate mapping if it doesn't exist or
// combines a delegate to an existing EventKey
public void Add(EventKey eventKey, Delegate handler)
{
// 线程安全的方式
Monitor.Enter(m_events);
Delegate d;
m_events.TryGetValue(eventKey, out d);
// 合并新的委托到委托链表中
// Delegate.Combine将`委托实例添加到委托列表`中,返回`新的列表头地址`, 将这个地址存回字段.
m_events[eventKey] = Delegate.Combine(d, handler);
Monitor.Exit(m_events);
}
// Removes a delegate from an EventKey (if it exists) and
// removes the EventKey -> Delegate mapping the last delegate is removed
public void Remove(EventKey eventKey, Delegate handler)
{
Monitor.Enter(m_events);
// Call TryGetValue to ensure that an exception is not thrown if
// attempting to remove a delegate from an EventKey not in the set
Delegate d;
// 确保从集合中删除不存在的EventKey时不会抛出异常.
if (m_events.TryGetValue(eventKey, out d))
{
d = Delegate.Remove(d, handler);
// 如果委托链表还存在委托,则重新设置头部的地址.
if (d != null) m_events[eventKey] = d;
// 链表为空,则移除这个键值对.
else m_events.Remove(eventKey);
}
Monitor.Exit(m_events);
}
// 为指定的EventKey引发事件
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)
{
// 由于字典可能包含几个不同的类型委托
// 所以无法再编译时构造一个类型安全的委托调用
// 调用Delegate的DynamicInvoke方法,以一个对象数组的形式传递回调方法的参数
// 在内部,DynamicInvoke会向调用的回调方法查证参数的类型安全性.
// 如果类型不匹配,会抛出异常
d.DynamicInvoke(new Object[] {sender, e});
}
}
}
使用EventSet类在TypeWithLotsOfEvents类中
TypeWithLotsOfEvents类(使用了大量时间的类):
using System;
// 为事件定义从EventArgs派生的附加信息类
public class FooEventArgs : EventArgs
{
}
// Define the EventArgs-derived type for this event.
public class BarEventArgs : EventArgs
{
}
///////////////////////////////////////////////////////////////////////////////
internal class TypeWithLotsOfEvents
{
// 定义私有只读字段来引用集合类
// 用于管理维护一组EventKey->Delegate(事件/委托)对.
private readonly EventSet m_eventSet = new EventSet();
// protected只能派生类型能访问集合
protected EventSet EventSet
{
get { return m_eventSet; }
}
// 定义Foo事件的键
// 2a. 构造静态只读对象来作为标识符表示这个事件
// 每个对象都有自己的哈希码,可以方便在对象的集合中查找这个事件的委托链表
protected static readonly EventKey s_fooEventKey = new EventKey();
// 2d. 定义事件的访问器, 用于在集合中增删委托
public event EventHandler<FooEventArgs> Foo
{
// 操作EventSet集合来 增加/删除 键值对
add { m_eventSet.Add(s_fooEventKey, value); }
remove { m_eventSet.Remove(s_fooEventKey, value); }
}
// 2e. 定义虚方法,用来触发事件
protected virtual void OnFoo(FooEventArgs e)
{
m_eventSet.Raise(s_fooEventKey, this, e);
}
// 2f. 定义将输入转成这个事件的方法
public void SimulateFoo()
{
OnFoo(new FooEventArgs());
}
}
使用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..");
}