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 threadsafe 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 typesafe 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, readonly 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..."); } }
浙公网安备 33010602011771号