CLR via C#, 4th -- 【设计类型】 -- 第11章 事 件

定义了事件成员的类型能提供以下功能。

  • 方法能登记它对事件的关注。
  • 方法能注销它对事件的关注。
  • 事件发生时,登记了的方法将收到通知。

类型之所以能提供事件通知功能,是因为类型维护了一个已登记方法的列表。事件发生后,类型将通知列表中所有已登记的方法。

CLR事件模型以委托为基础。委托是调用(在英语的语境中,invoke和call的区别在于,在执行一个所有信息都已知的方法时,用call比较恰当。这些信息包括要引用的类型、方法的签名以及方法名。但是,在需要先“唤出”某个东西来帮你调用一个信息不明的方法时,用invoke就比较恰当。)回调方法的一种类型安全的方式。对象凭借回调方法接收它们订阅的通知。

11.1 设计要公开事件的类型

11.1.1 第一步:定义类型来容纳所有需要发送给事件通知接收者的附加信息

事件引发时,引发事件的对象可能希望向接收事件通知的对象传递一些附加信息。这些附加信息需要封装到它自己的类中,该类通常包含一组私有字段,以及一些用于公开这些字段的只读公共属性。

// Step #1: Define a type that will hold any additional information that 
// should be sent to receivers of the event notification    
internal 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; } }  
}

11.1.2 第二步:定义事件成员

事件成员使用C#关键字event定义。每个事件成员都要指定以下内容:可访问性标识符(几乎肯定是public,这样其他代码才能访问该事件成员);委托类型,指出要调用的方法的原型:以及名称(可以是任何有效的标识符)。

internal class MailManager {  
 
   // Step #2: Define the event member   
   public event EventHandler<NewMailEventArgs> NewMail;   
   ...  
}

NewMail是事件名称。事件成员的类型是EventHandler<NewMailEventArgs>,意味着“事件通知”的所有接收者都必须提供一个原型和EventHandler<NewMailEventArgs委托类型匹配的回调方法。由于泛型System.EventHandler委托类型的定义如下:

public delegate void EventHandler<TEventArgs>(Object sender, TEventArgs e);

所以方法原型必须具有以下形式:

void MethodName(Object sender, NewMailEventArgs e);

11.1.3 第三步:定义负责引发事件的方法来通知事件的登记对象

按照约定,类要定义一个受保护的虚方法。引发事件时,类及其派生类中的代码会调用该方法。方法只获取一个参数,其中包含了传给接收通知的对象的信息。方法的默认实现只是检查一下是否有对象登记了对事件的关注。如果有,就引发事件来通知事件的登记对象。

internal class MailManager {  
   ...  
   // Step #3: Define a method responsible for raising the event    
   // to notify registered objects that the event has occurred   
   // If this class is sealed, make this method private and nonvirtual   
   protected virtual void OnNewMail(NewMailEventArgs e) {   
 
      // Copy a reference to the delegate field now into a temporary field for thread safety   
      EventHandler<NewMailEventArgs> temp = Volatile.Read(ref NewMail);  
 
      // If any methods registered interest with our event, notify them    
      if (temp != null) temp(this, e);  
   }  
   ...  
}

为方便起见,可定义扩展方法(参见第8章“方法”)来封装这个线程安全逻辑。如下所示:

public static class EventArgExtensions { 
   public static void Raise<TEventArgs>(this TEventArgs e,  
      Object sender, ref EventHandler<TEventArgs> eventDelegate) {  
 
      // Copy a reference to the delegate field now into a temporary field for thread safety   
      EventHandler<TEventArgs> temp = Volatile.Read(ref eventDelegate);  
 
      // If any methods registered interest with our event, notify them    
      if (temp != null) temp(sender, e); 
   } 
}

现在可以像下面这样重写OnNewMail方法:

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

11.1.4 第四步:定义方法将输入转化为期望事件

类还必须有一个方法获取输入并转化为事件的引发。

internal class MailManager {  
 
   // Step #4: Define a method that translates the   
   // input into the desired event  
   public void SimulateNewMail(String from, String to, String subject) {   
 
      // Construct an object to hold the information we want   
      // to pass to the receivers of our notification  
      NewMailEventArgs e = new NewMailEventArgs(from, to, subject);   
 
      // Call our virtual method notifying our object that the event  
      // occurred. If no type overrides this method, our object will  
      // notify all the objects that registered interest in the event  
      OnNewMail(e);  
   }      
}

事件模式为什么要求sender参数是Object类型

  • 要求sender是Object主要是因为继承。
  • 将sender参数的类型定为Object的另一个原因是灵活性。
  • 此外,事件模式要求委托定义和回调方法将派生自EventArgs的参数命名为e.这个要求唯一的作用就是加强事件模式的一致性,使开发人员更容易学习和实现这个模式。注意,能自动生成源代码的工具(比如Microsoft Visual Studio)也知道将参数命名为e.
  • 最后,事件模式要求所有事件处理程序的返回类型都是void.这很有必要,因为引发事件后可能要调用好几个回调方法,但没办法获得所有方法的返回值。

以线程安全的方式引发事件

// Version 1 
protected virtual void OnNewMail(NewMailEventArgs e) { 
   if (NewMail != null) NewMail(this, e);  
}

OnNewMail方法的问题在于,虽然线程检查出NewMail不为null,但就在调用NewMail之前,另一个线程可能从委托链中移除一个委托,使NewMail成了null.这会抛出NullReferenceException异常。为了修正这个竞态问题,许多开发者都像下面这样写OnNewMail方法

// Version 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的方式对上述代码进行优化。如果发生这种情况,版本2就和版本1就没有任何区别。所以,仍有可能抛出NullReferenceException异常.

// Version 3 
protected virtual void OnNewMail(NewMailEventArgs e) { 
   EventHandler<NewMailEventArgs> temp = Volatile.Read(ref NewMail);  
   if (temp != null) temp(this, e); 
}

对Volatile.Read的调用强迫NewMail在这个调用发生时读取,引用真的必须复制到temp变量中(编译器别想走捷径),然后,temp变量只有在不为null时才会被调用(invoke).第29章“基元线程同步构造”将详细讨论Volatile.Read方法。

11.2 编器如何实现事件

public event EventHandler<NewMailEventArgs> NewMail;

C#编译器编译时把它转换为以下3个构造:

// 1. A PRIVATE delegate field that is initialized to null   
private EventHandler<NewMailEventArgs> NewMail = null;  
 
// 2. A PUBLIC add_Xxx method (where Xxx is the Event name)  
// Allows methods to register interest in the event.   
public void add_NewMail(EventHandler<NewMailEventArgs> value) {  
   // The loop and the call to CompareExchange is all just a fancy way   
   // of adding a delegate to the event in a thread ­safe way  
   EventHandler<NewMailEventArgs>prevHandler; 
      EventHandler<NewMailEventArgs> newMail = this.NewMail; 
   do { 
      prevHandler = newMail;  
      EventHandler<NewMailEventArgs> newHandler =  
         (EventHandler<NewMailEventArgs>) Delegate.Combine(prevHandler, value); 
      newMail = Interlocked.CompareExchange<EventHandler<NewMailEventArgs>>(  
         ref this.NewMail, newHandler, prevHandler); 
   } while (newMail != prevHandler);  
}  
 
// 3. A PUBLIC remove_Xxx method (where Xxx is the Event name)   
// Allows methods to unregister interest in the event.  
public void remove_NewMail(EventHandler<NewMailEventArgs> value) {  
   // The loop and the call to CompareExchange is all just a fancy way   
   // of removing a delegate from the event in a thread­safe way  
   EventHandler<NewMailEventArgs> prevHandler;  
   EventHandler<NewMailEventArgs> newMail = this.NewMail; 
   do { 
      prevHandler = newMail;  
      EventHandler<NewMailEventArgs> newHandler =  
         (EventHandler<NewMailEventArgs>) Delegate.Remove(prevHandler, value);  
      newMail = Interlocked.CompareExchange<EventHandler<NewMailEventArgs>>(  
         ref this.NewMail, newHandler, prevHandler); 
   } while (newMail != prevHandler);  
}

第一个构造是具有恰当委托类型的字段。该字段是对一个委托列表的头部的引用。事件发生时会通知这个列表中的委托。字段初始化为null,表明无侦听者(listener)登记对该事件的关注。

注意,即使原始代码行将事件定义为public,委托字段(本例是NewMail)也始终是private.将委托字段定义为private,目的是防止类外部的代码不正确地操纵它。如果字段是public,任何代码都能更改字段中的值,并可能删除已登记了对事件的关注的委托。

C#编译器生成的第二个构造是一个方法,允许其他对象登记对事件的关注。C#编译器在事件名之前附加add_前缀,从而自动命名该方法。C#编译器还自动为方法生成代码。生成的代码总是调用System.Delegate的静态Combine方法,它将委托实例添加到委托列表中,返回新的列表头(地址),并将这个地址存回字段。
C#编译器生成的第三个构造是一个方法,允许对象注销对事件的关注。同样地,C#编译器在事件名之前附加remove-前级,从而自动命名该方法。方法中的代码总是调用Delegate的静态Remove方法,将委托实例从委托列表中删除,返回新的列表头(地址),并将这个地址存回字段。
注意,即使原始代码行将事件定义为public,委托字段也始终是private.将委托字段定义为private,目的是防止类外部的代码不正确地操纵它。如果字段是public,任何代码都能更改字段中的值,并可能删除已登记了对事件的关注的委托。

11.3 设计侦听事件的类型

internal sealed class Fax {   
   // Pass the MailManager object to the constructor   
   public Fax(MailManager mm) {  
 
      // Construct an instance of the EventHandler<NewMailEventArgs>   
      // delegate that refers to our FaxMsg callback method.   
      // Register our callback with MailManager's NewMail event  
      mm.NewMail += FaxMsg;   
   }  
 
   // This is the method the MailManager will call   
   // when a new email message arrives  
   private void FaxMsg(Object sender, NewMailEventArgs e) {  
       // 'sender' identifies the MailManager object in case    
      // we want to communicate back to it.  
 
      // 'e' identifies the additional event information    
      // the MailManager wants to give us.   
 
      // Normally, the code here would fax the email message.  
      // This test implementation displays the info in the console  
      Console.WriteLine("Faxing mail message:");  
      Console.WriteLine("   From={0}, To={1}, Subject={2}",  
         e.From, e.To, e.Subject);  
   }  
 
   // This method could be executed to have the Fax object unregister    
   // itself with the NewMail event so that it no longer receives   
   // notifications  
   public void Unregister(MailManager mm) {  
 
      // Unregister with MailManager's NewMail event   
      mm.NewMail ­= FaxMsg;  
   }  
}

C#编译器内建了对事件的支持,会将+=操作符翻译成以下代码来添加对象对事件的关注:

mm.add_NewMail(new EventHandler<NewMailEventArgs>(this.FaxMsg));

MailManager对象引发事件时,Fax对象的FaxMsg方法会被调用。调用这个方法时,会传递MailManager对象引用作为它的第一个参数,即sender,该参数大多数时候会被忽略。
但如果Fax对象希望在响应事件时访问MailManager对象的成员,它就能派上用场了。第二个参数是NewMailEventArgs对象引用。对象中包含MailManager和NewMailEventArgs的设计者认为对事件接收者来说有用的附加信息。

11.4 显式实现事件

System.Windows.Forms.Control类型定义了大约70个事件。由于大多数程序员只关心少数几个事件,所以每个从Control派生类型创建的对象都要浪费大量内存。
为了高效率存储事件委托,公开了事件的每个对象都要维护一个集合(通常是字典)。集合将某种形式的事件标识符作为键(key),将委托列表作为值(value),新对象构造时,这个集合是空白的。登记对一个事件的关注时,会在集合中查找事件的标识符。如果事件标识符已在其中,新委托就和这个事件的委托列表合并。如果事件标识符不在集合中,就添加事件标识符和委托。
对象需要引发事件时,会在集合中查找事件标识符。如果集合中没有找到事件标识符,表明还没有任何对象登记对这个事件的关注,所以没有任何委托需要回调。如果事件标识符在集合中,就调用与它关联的委托列表。

using System;  
using System.Collections.Generic;  
 
// This class exists to provide a bit more type safety and  
// code maintainability when using EventSet  
public sealed class EventKey { } 
 
public sealed class EventSet { 
   // The private dictionary used to maintain EventKey ­> Delegate mappings 
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);  
   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; 
   if (m_events.TryGetValue(eventKey, out d)) { 
      d = Delegate.Remove(d, handler);  
       // If a delegate remains, set the new head else remove the EventKey  
      if (d != null) m_events[eventKey] = d; 
      else m_events.Remove(eventKey); 
   } 
   Monitor.Exit(m_events);  
} 
 
// Raises the event for the indicated EventKey  
public void Raise(EventKey eventKey, Object sender, EventArgs e) {  
   // Don't throw an exception if the EventKey is not in the set 
   Delegate d; 
   Monitor.Enter(m_events); 
   m_events.TryGetValue(eventKey, out d);  
   Monitor.Exit(m_events);  
 
   if (d != null) { 
      // Because the dictionary can contain several different delegate types, 
      // it is impossible to construct a type­safe call to the delegate at  
      // compile time. So, I call the System.Delegate type's DynamicInvoke  
      // method, passing it the callback method's parameters as an array of   
      // objects. Internally, DynamicInvoke will check the type safety of the   
      // parameters with the callback method being called and call the method.  
      // If there is a type mismatch, then DynamicInvoke will throw an exception.  
      d.DynamicInvoke(new Object[] { sender, e }); 
      } 
}
using System;  
 
// Define the EventArgs ­derived type for this event.  
public class FooEventArgs : EventArgs { }  
 
public class TypeWithLotsOfEvents { 
 
   // Define a private instance field that references a collection. 
   // The collection manages a set of Event/Delegate pairs. 
   // NOTE: The EventSet type is not part of the FCL, it is my own type. 
   private readonly EventSet m_eventSet = new EventSet(); 
    // The protected property allows derived types access to the collection. 
   protected EventSet EventSet { get { return m_eventSet; } }  
 
   #region Code to support the Foo event (repeat this pattern for additional events) 
   // Define the members necessary for the Foo event.  
   // 2a. Construct a static, read­only object to identify this event. 
   // Each object has its own hash code for looking up this 
   // event's delegate linked list in the object's collection. 
   protected static readonly EventKey s_fooEventKey = new EventKey(); 
 
   // 2b. Define the event's accessor methods that add/remove the 
   // delegate from the collection. 
   public event EventHandler<FooEventArgs> Foo {  
      add    { m_eventSet.Add(s_fooEventKey, value); } 
      remove { m_eventSet.Remove(s_fooEventKey, value); } 
 
   } 
 
   // 2c. Define the protected, virtual On method for this event. 
   protected virtual void OnFoo(FooEventArgs e) { 
      m_eventSet.Raise(s_fooEventKey, this, e); 
   } 
 
   // 2d. Define the method that translates input to this event. 
   public void SimulateFoo() { OnFoo(new FooEventArgs()); } 
   #endregion  
}
public sealed class Program { 
   public static void Main() { 
      TypeWithLotsOfEvents twle = new TypeWithLotsOfEvents();  
 
      // Add a callback here  
      twle.Foo += HandleFooEvent;  
 
      // Prove that it worked 
      twle.SimulateFoo(); 
   } 
 
   private static void HandleFooEvent(object sender, FooEventArgs e) { 
      Console.WriteLine("Handling Foo Event here..."); 
   } 
}

 

posted @ 2019-12-10 23:55  FH1004322  阅读(142)  评论(0)    收藏  举报