C#事件

定义一个事件成员,表示该类型提供了如下功能:

1.能够在事件中注册方法   2.能够在事件中注销方法   3.当事件发生时,注册的方法会被通知

(事件内部维护了一个注册方法列表)

CLR的事件模型是基于委托的,它可以通过类型安全的方式调用回调方法。而回调方法是订阅事件的对象接收通知的方式。通过一个例子来说明:

①Fax对象的方法注册到MailManager事件   ②Pager对象的方法注册到MailManager事件   ③新的邮件到达MailManager   ④MailManager对象向注册的方法发出通知,接收通知的方法可以随意处理。

具体实现步骤如下:

1.定义一个类型,能够hold住任何发送到事件通知接收者的信息

当一个事件被触发,触发事件的对象可能希望发送一些额外的信息给事件通知的接收对象。这些额外的信息需要封装在它自己的类中,根据约定该类需要从System.EventArgs类派生,并且命名以EventArgs结尾。这里定义一个NewMailEventArgs类:

public 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;
 }

 public String From { get { return m_from; } }
 public String To { get { return m_to; } }
 public String Subject { get { return m_subject; } }

 }

关于EventArgs

[ComVisible(true)]
[Serializable]
public class EventArgs
{
 public readonly static EventArgs Empty;

 static EventArgs()
 {
 EventArgs.Empty = new EventArgs();
 }

 public EventArgs()
 {
 }
}

这个类没有实际的用途,只是作为一个基类让其他对象继承。很多对象不需要传递额外的信息,例如按钮事件,只是调用一个回调方法就够了。当我们定义的事件不需要传递额外的信息时,这时调用EventArgs.Empty就行了,不需要重新构建一个EventArgs对象。

2.定义事件成员

public class MailManager
 {
 ...
 //NewMail事件名,
 //EventHanlder,所有的事件通知接收对象必须提供给该委托类型匹配的回调方法
 public event EventHandler NewMail;
 }

System.EventHandler委托的定义为:public  delegate  void EventHandler(Object  sender, TEventArgs e) where  TEventArgs:  EventArgs;

为什么这里第一个参数sender的类型是Object?毕竟MailManager类型是唯一触发这个事件的,所以可以设计成这样:
void MethodName(MailManager sender,NewMailEventArgs e)
这种情况会有一个弊端,当sender是SmtpMailManager时,回调方法也需要改变,使用Object能够很好的兼容。定义回调方法的参数名约定为e,这样做主要是为了保持一致性。方便开发人员。

事件机制要求所有的事件处理方法必须返回void,这是必要的,因为一个事件可能触发很多的回调方法,没有办法获取所有的返回值,索性就不允许返回值,全部为void。有些FCL里面的事件处理程序没有遵循,而是返回了一个Assembly类型。

3.定义一个方法来响应事件的发生

按照惯例,这个类应该定义一个protected,virtual的方法供内部的代码调用。这个方法接收一个NewMailEventArgs对象,这个对象包含要传递给消息接收方的一些信息。如下:

protected virtual void OnNewMail(NewMailEventArgs e)
 {
 //复制一个委托的引用到临时字段temp,这样确保线程安全
 EventHandler temp = Interlocked.CompareExchange(ref NewMail, null, null);

 //任何注册到事件里面的方法,通知它们
 if (temp != null)
 {
 temp(this, e);
 }
 }

Tips:使用线程安全的方式触发事件(①——>④为不断改进的过程)

①当.NET第一次推出的时候,给开发者推荐的事件触发方式如下:

//v1.0
protected virtual void OnNewMail(NewMailEventArgs e) 
{ 
 if (NewMail != null) 
 {
 NewMail(this, e); 
 } 
}

弊端:这里检查了NewMail不为null才触发,但是当检查完之后,在调用NewMail之前,有其他的线程从委托链中移除了一个委托,使得NewMail为null,此时会抛出异常。

②先将NewMail用一个临时变量存起来,这时就不会因为调用时被其他线程修改而抛出异常。之所以能够这样做,是因为委托类型跟字符串类型一样是不可变的。

//v2.0
protected void OnNewMail(NewMailEventArgs e) 
{ 
 EventHandler temp = NewMail; 
 if (temp != null)
 {
 temp(this, e); 
 } 
}

弊端:可能被编译器优化掉本地temp变量,如果发生这种情况,就回到了第一种了。

③修复上面的bug,如下:

//v3.0
protected void OnNewMail(NewMailEventArgs e) 
{ 
 EventHandler temp = Thread.VolatileRead(ref NewMail); 
 if (temp != null)
 { 
 temp(this, e);
 } 
}

这里使用VolatileRead会强制读取temp的值,但是这里不能这样写,编译不通过。但是有一个Interlocked.CompareExchange可以使用:

 //v4.0 
 protected virtual void OnNewMail(NewMailEventArgs e)
 {
 //复制一个委托的引用到临时字段temp,这样确保线程安全
 EventHandler temp = Interlocked.CompareExchange(ref NewMail, null, null);

 //任何注册到事件里面的方法,通知它们
 if (temp != null)
 {
 temp(this, e);
 }
 }

如果NewMail为null,CompareExchange将NewMail的值改变为null,如果不为null则返回原值。换句话说,CompareExchange不会改变NewMail的值,只是以线程安全的方式返回NewMail的值,这里是一个原子操作。

第④个版本是最佳的,技术上最正确的版本。实际开发中还是可以使用第②个版本,因为JIT编译器能够识别这种模式而不去优化本地的temp变量。特别地,所有微软的JIT编译器都遵循不会对堆引入新的读取,因此缓存一个引用在本地变量可以确保堆引用只被访问一次(这是没有写入文档的,理论上,还是可能发生变化,所以最好选用第④版本。)

为了方便可以定义一个扩展方法来封装:

 public static class EventArgExtensions
 {
 public static void Raise(this TEventArgs e, Object sender, ref EventHandler eventDelegate) 
where TEventArgs : EventArgs
 {
 EventHandler temp = Interlocked.CompareExchange(ref eventDelegate, null, null);
 if (temp != null)
 {
 temp(sender, e);
 }
 }
 }

然后可以重写OnNewMail:

 protected virtual void OnNewMail(NewMailEventArgs e)
 {
 e.Raise(this, ref NewMail);
 }

4.定义一个方法用来传递一些输入到事件

 public void SimulateNewMail(String from, String to, String subject)
 {
 NewMailEventArgs e = new NewMailEventArgs(from, to, subject);
 OnNewMail(e);
 }

编译器是怎么实现事件的?

在MailManager里面用一行定义了一个事件:public  event EventHandler NewMail;
编译器会将上面一行代码编译为三个部分如下:

// 1. 初始化一个私有的委托变量
private EventHandler NewMail = null; 
 
// 2. 将方法注册到事件的public方法add_Xxx (Xxx表示事件名) 
public void add_NewMail(EventHandler value) 
{ 
 // 循环和调用CompareExchange是一种添加委托的线程安全的方式 
 EventHandler prevHandler; 
 EventHandler newMail = this.NewMail; 
 do { 
 prevHandler = newMail; 
 EventHandlernewHandler = 
 (EventHandler) Delegate.Combine(prevHandler, value); 
 newMail = Interlocked.CompareExchange<EventHandler>( 
 ref this.NewMail, newHandler, prevHandler); 
 } while (newMail != prevHandler); 
} 
// 3. public方法remove_Xxx(Xxx表示方法名) 
// 允许方法从事件中注销public void remove_NewMail(EventHandler value) 
{ 
 EventHandler prevHandler; 
 EventHandler newMail = this.NewMail; 
 do { 
 prevHandler = newMail; 
 EventHandler newHandler = 
 (EventHandler) Delegate.Remove(prevHandler, value); 
 newMail = Interlocked.CompareExchange<EventHandler>( 
 ref this.NewMail, newHandler, prevHandler); 
 } while (newMail != prevHandler); 
}

从这里可以看出事件的确是一块语法糖,这里首先定义了一个私有的委托变量,接着是对该对象增加add和remove操作。所以事件是对委托的封装,来限制我们只能对委托进行add和remove操作,外界并不能访问委托变量本身(私有的)。

注:

1.如果Remove一个未添加的方法,Delegate.Remove在内部不会执行任何操作,也不会抛异常,事件的方法集合不会发送变化。

2.在这个里面add和remove方法都是public的,原因是定义event NewMail时是public的,它们的可访问性保持一致。Event成员也可以被定义为static或virtual,此时,编译器生成的add和remove方法也是static或virtual

3.除了上面列举的三个部分,编译器还会生成一个事件定义的入口。该入口包含一些标志位和在事件下的委托类型,以及add和remove访问器的引用。这个信息能够简单的描述抽象的事件概念和访问器方法的联系。

定义一个类型来监听事件

 public sealed class Fax
 {
 public Fax(MailManager mm)
 {
 // 注册回调方法FaxMsg
 mm.NewMail += FaxMsg;
 }

 // 当NewEmail发生时,触发这个方法执行
 private void FaxMsg(Object sender, NewMailEventArgs e)
 {
 // 'sender'用来识别是MailManager对象
 // 'e' MailManager要传递的额外的事件信息
 Console.WriteLine("Faxing mail message:");
 Console.WriteLine(" From={0}, To={1}, Subject={2}",
 e.From, e.To, e.Subject);
 }

 //从事件从注销方法,以后不在收到通知
 public void Unregister(MailManager mm)
 {
 mm.NewMail -= FaxMsg;
 }
 }

C#内置对事件的支持,编译会根据+=生成代码:mm.add_NewMail(new  EventHandler(this.FaxMsg));

编译器构造了一个EventHandler委托对象来封装Fax类的FaxMsg方法。然后,C#编译器调用MailManager的add_NewMail方法,将它传递到新的委托。IL如下:

.event class [mscorlib]System.EventHandler`1<class TypeTest.NewMailEventArgs> NewMail
 {
 .addon instance void TypeTest.MailManager::add_NewMail(class [mscorlib]System.EventHandler`1<class TypeTest.NewMailEventArgs>)
 {
 .locals init (
 [0] class [mscorlib]System.EventHandler`1<class TypeTest.NewMailEventArgs> V_0,
 [1] class [mscorlib]System.EventHandler`1<class TypeTest.NewMailEventArgs> V_1,
 [2] class [mscorlib]System.EventHandler`1<class TypeTest.NewMailEventArgs> V_2,
 [3] bool V_3
 )

 IL_0000: ldarg.0
 IL_0001: ldfld class [mscorlib]System.EventHandler`1<class TypeTest.NewMailEventArgs> TypeTest.MailManager::NewMail
 IL_0006: stloc.0
 .loop
 {
 IL_0007: ldloc.0
 IL_0008: stloc.1
 IL_0009: ldloc.1
 IL_000a: ldarg.1
 IL_000b: call class [mscorlib]System.Delegate [mscorlib]System.Delegate::Combine(class [mscorlib]System.Delegate, class [mscorlib]System.Delegate)
 IL_0010: castclass class [mscorlib]System.EventHandler`1<class TypeTest.NewMailEventArgs>
 IL_0015: stloc.2
 IL_0016: ldarg.0
 IL_0017: ldflda class [mscorlib]System.EventHandler`1<class TypeTest.NewMailEventArgs> TypeTest.MailManager::NewMail
 IL_001c: ldloc.2
 IL_001d: ldloc.1
 IL_001e: call class [mscorlib]System.EventHandler`1<class TypeTest.NewMailEventArgs> [mscorlib]System.Threading.Interlocked::CompareExchange<class [mscorlib]System.EventHandler`1<class TypeTest.NewMailEventArgs>>(!!0&, !!0, !!0)
 IL_0023: stloc.0
 IL_0024: ldloc.0
 IL_0025: ldloc.1
 IL_0026: ceq
 IL_0028: ldc.i4.0
 IL_0029: ceq
 IL_002b: stloc.3
 IL_002c: ldloc.3
 IL_002d: brtrue.s IL_0007
 }
 IL_002f: ret
 }
 .removeon instance void TypeTest.MailManager::remove_NewMail(class [mscorlib]System.EventHandler`1<class TypeTest.NewMailEventArgs>)
 {
 .locals init (
 [0] class [mscorlib]System.EventHandler`1<class TypeTest.NewMailEventArgs> V_0,
 [1] class [mscorlib]System.EventHandler`1<class TypeTest.NewMailEventArgs> V_1,
 [2] class [mscorlib]System.EventHandler`1<class TypeTest.NewMailEventArgs> V_2,
 [3] bool V_3
 )

 IL_0000: ldarg.0
 IL_0001: ldfld class [mscorlib]System.EventHandler`1<class TypeTest.NewMailEventArgs> TypeTest.MailManager::NewMail
 IL_0006: stloc.0
 .loop
 {
 IL_0007: ldloc.0
 IL_0008: stloc.1
 IL_0009: ldloc.1
 IL_000a: ldarg.1
 IL_000b: call class [mscorlib]System.Delegate [mscorlib]System.Delegate::Remove(class [mscorlib]System.Delegate, class [mscorlib]System.Delegate)
 IL_0010: castclass class [mscorlib]System.EventHandler`1<class TypeTest.NewMailEventArgs>
 IL_0015: stloc.2
 IL_0016: ldarg.0
 IL_0017: ldflda class [mscorlib]System.EventHandler`1<class TypeTest.NewMailEventArgs> TypeTest.MailManager::NewMail
 IL_001c: ldloc.2
 IL_001d: ldloc.1
 IL_001e: call class [mscorlib]System.EventHandler`1<class TypeTest.NewMailEventArgs> [mscorlib]System.Threading.Interlocked::CompareExchange<class [mscorlib]System.EventHandler`1<class TypeTest.NewMailEventArgs>>(!!0&, !!0, !!0)
 IL_0023: stloc.0
 IL_0024: ldloc.0
 IL_0025: ldloc.1
 IL_0026: ceq
 IL_0028: ldc.i4.0
 IL_0029: ceq
 IL_002b: stloc.3
 IL_002c: ldloc.3
 IL_002d: brtrue.s IL_0007
 }
 IL_002f: ret
 }
 }

当MailManager对象触发了事件,Fax对象的FaxMsg方法会被调用,这个方法的一个引用会被作为MailManager对象的第一个参数sender传递给MailManager。多数时候该参数被忽略。当Fax对象想访问MailManager对象来响应事件通知时会使用。第二个参数是NewMailEventArgs对象,传递额外的信息。

当一个对象不再希望接收某事件的通知时,应该从事件里面注销。只要某一个对象的任何一个方法还在事件里面,这个对象就不会被GC回收。如果类型实现了IDisposable的Dispose方法,该实现应该注销所有在事件里面的方法。

注   《CLR via C#》(Jeffrey Richter著)——.NET 界的经典之作,读的过程写点笔记跟大家分享,我也推荐大家看英文版,能够直接领会原意 

作者:张雪飞
出处:https://zhangxuefei.site/p/91
版权说明:欢迎转载,但必须注明出处,并在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

发表评论

电子邮件地址不会被公开。 必填项已用*标注