CLR事件模型以委托为基础。委托是调用回调方法的一种类型安全的方式。对象凭借回调方法接受他们订阅的通知。
定义了事件成员的类型能提供以下功能:
1. 方法能登记它对事件的关注
2. 方法能注销它对事件的关注
3. 事件发生时,登记了的方法将收到通知
第一步: 定义类型来容纳所有需要发送给事件通知接收者的附加信息
第二步: 定义事件成员
第三步: 定义负责引发事件的方法来通知事件的登记对象
第四步: 定义方法将输入转化为期望事件
//第一步: 定义类型来容纳所有需要发送给事件通知接收者的附加信息
internal class NewMailEventArgs: EventArgs
{
private readonly string m_from, m_to, m_subject;
public NewMailEventArgs(string from, string to, string subject)
{
this.m_from = from;
this.m_to = to;
this.m_subject = subject;
}
public string From { get { return m_from; } }
public string To { get { return m_to; } }
public string Subject { get { return m_subject; } }
}
internal class MailManager
{
//第二步: 定义事件成员
public event EventHandler<NewMailEventArgs> NewMail;
//第三步,定义负责引发事件的方法来通知已登记的对象
//如果类是密封的,该方法要声明为私有和非虚
protected virtual void OnNewMail(NewMailEventArgs e)
{
//出于线程安全的考录,现在将对象委托字段的引用复制到一个临时变量中
EventHandler<NewMailEventArgs> temp = Volatile.Read(ref NewMail);
//任何方法登记了对事件的关注,就通知它们
if (temp != null) temp(this, e);
}
//第四步: 定义方法将输入转化为期望事件
public void SimulateNewMail(string from, string to, string subject)
{
NewMailEventArgs e = new NewMailEventArgs(from, to, subject);
OnNewMail(e);
}
}
注意:
第三步中采用了以线程安全的方式引发事件
.NET Framework 刚发布时建议开发者用以下方式引发事件:
//版本1
protected virtual void OnNewMail(NewMailEventArgs e)
{
if (NewMail != null) NewMail(this, e);
}
OnNewMail方法的问题在于,虽然线程检查出NewMail不为null, 但就在调用NewMail之前,另一个线程可能从委托链中移除一个委托,使NewMail成了null。这会抛出NullReferenceException异常。为了修正这个竟态问题,许多开发者都像下面这样写OnNewMail方法
//版本2
protected virtual void OnNewMail(NewMailEventArgs e)
{
EventHandler<NewMailEventArgs> temp = NewMail;
if (temp != null) temp(this, e);
}
它的思路是,将对NewMail的引用复制到临时变量temp中,后者引用赋值发生时的委托链。然后,方法比较temp和null,并调用(invoke)temp;所以,向temp赋值后,即使另一个线程更改了NewMail也没有关系;委托是不可变的(immutable),所以这个技术理论上行得通。但许多开发者没有意思到的是,编译器可能“擅作主张”,通过完全移除局部变量temp的方式对上述代码进行优化。
想要修正这个问题,应该像下面这样重写OnNewMail
//版本三
protected virtual void OnNewMail(NewMailEventArgs e)
{
//出于线程安全的考录,现在将对象委托字段的引用复制到一个临时变量中
EventHandler<NewMailEventArgs> temp = Volatile.Read(ref NewMail);
//任何方法登记了对事件的关注,就通知它们
if (temp != null) temp(this, e);
}
显示实现事件
为了高效率存储事件委托,公开了事件的每个对象都要维护一个集合(通常是字典)。集合将某种形式的事件标识符作为键(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不存在)
//或者将委托和现有的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(如果它存在)删除委托, 并且
//在删除最后一个委托时删除EventKey -> Delegate映射
public void Remove(EventKey eventKey, Delegate handler)
{
Monitor.Enter(m_events);
//调用TryGetValue,确保在尝试从集合中删除不存在的EventKey时不会抛出异常
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);
}
//为指定的EventKey引发事件
public void Raise(EventKey eventKey, Object sender, EventArgs e)
{
Delegate d;
Monitor.Enter(m_events);
m_events.TryGetValue(eventKey, out d);
Monitor.Exit(m_events);
if (d != null)
{
d.DynamicInvoke(new Object[] { sender, e });
}
}
}
接着定义一个类来使用EventSet类。在这个类中,一个字段引用了一个EventSet对象,而且这个类的每个事件都是现实实现的,使每个事件的add方法都将指定的回调委托存储到EventSet对象中,而且每个事件的remove方法都删除指定的回调委托(如果找得到的话)。
public class FooEventArgs: EventArgs { }
public class TypeWithLotsOfEvents
{
//定义私有实例字段来引用集合
//集合用于管理一组“事件/委托”对
//注意: EventSet类型不是FCL的一部分,他是我自己的类型
private readonly EventSet m_eventSet = new EventSet();
//受保护的属性使派生类能访问集合
protected EventSet EventSet { get { return m_eventSet; } }
#region 用于支持Foo事件的代码(为附加的事件重复这个模式)
//定义Foo事件必要的成员
//2a. 构造一个静态只读对象来标识这个事件
//每个对象都有自己的哈希码, 以便在对象的集合中查找这个事件的委托链表
protected static readonly EventKey s_fooEventKey = new EventKey();
//2b 定义事件的访问器方法,用于在集合中增删委托
public event EventHandler<FooEventArgs> Foo {
add { m_eventSet.Add(s_fooEventKey, value); }
remove { m_eventSet.Remove(s_fooEventKey, value); }
}
//2c. 为这个事件定义受保护的虚方法OnFoo
protected virtual void OnFoo(FooEventArgs e)
{
m_eventSet.Raise(s_fooEventKey, this, e);
}
//2d. 定义将输入转换成这个事件的方法
public void SimulateFoo()
{
OnFoo(new FooEventArgs());
}
#endregion
}