# Chapter 11 Events
In this chapter, I’ll talk about the last kind of member a type can define: events. A type that defines an event member allows the type (or instances of the type) to notify other objects that something special has happened. For example, the Button class offers an event called Click. When a Button object is clicked, one or more objects in an application may want to receive notification about this event in order to perform some action. Events are type members that allow this interaction. Specifically, defining an event member means that a type is offering the following capabilities:
A method can register its interest in the event.
A method can unregister its interest in the event.
Registered methods will be notified when the event occurs.
Types can offer this functionality when defining an event because they maintain a list of the registered methods. When the event occurs, the type notifies all of the registered methods in the collection.
The common language runtime’s (CLR’s) event model is based on delegates. A delegate is a typesafe way to invoke a callback method. Callback methods are the means by which objects receive the notifications they subscribed to. In this chapter, I’ll be using delegates, but I won’t fully explain all their details until Chapter 17, “Delegates.”
To help you fully understand the way events work within the CLR, I’ll start with a scenario in which events are useful. Suppose you want to design an email application. When an email message arrives, the user might like the message to be forwarded to a fax machine or a pager. In architecting this application, let’s say that you’ll first design a type called MailManager that receives the incoming email messages. MailManager will expose an event called NewMail. Other types (such as Fax and Pager) may register interest in this event. When MailManager receives a new email message, it will raise the event, causing the message to be distributed to each of the registered objects. Each object can process the message in any way it desires.
When the application initializes, let’s instantiate just one MailManager instance—the application can then instantiate any number of Fax and Pager types. Figure 11-1 shows how the application initializes and what happens when a new email message arrives.
Here’s how the application illustrated in Figure 11-1 works: the application initializes by constructing an instance of MailManager. MailManager offers a NewMail event. When the Fax and Pager objects are constructed, they register an instance method with MailManager’s NewMail event so that MailManager knows to notify the Fax and Pager objects when new email messages arrive. Now, when MailManager receives a new email message (sometime in the future), it will raise the NewMail event, giving all of the registered methods an opportunity to process the new message in any way they want.
# Designing a Type That Exposes an Event
There are many steps a developer must take in order to define a type that exposes one or more event members. In this section, I’ll walk through each of the necessary steps. The MailManager sample application (which can be downloaded from the Books section in Resources at http://wintellect.com/ Books) shows all of the source code for the MailManager type, the Fax type, and the Pager type. You’ll notice that the Pager type is practically identical to the Fax type.
# Step #1: Define a type that will hold any additional information that should be sent to receivers of the event notification
When an event is raised, the object raising the event may want to pass some additional information to the objects receiving the event notification. This additional information needs to be encapsulated into its own class, which typically contains a bunch of private fields along with some read-only public properties to expose these fields. By convention, classes that hold event information to be passed to the event handler should be derived from System.EventArgs, and the name of the class should be suffixed with EventArgs. In this example, the NewMailEventArgs class has fields identifying who sent the message (m_from), who is receiving the message (m_to), and the subject of the message (m_subject).
// 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; } } | |
} |
💡注意: EventArgs
类在 Microsoft .NET Framework 类库 (FCL) 中定义,其实现如下:
[ComVisible(true), Serializable] | |
public class EventArgs { | |
public static readonly EventArgs Empty = new EventArgs(); | |
public EventArgs() { } | |
} |
可以看出,该类型的实现非常简单,就是一个让其他类型继承的基类型。许多事件都没有附加信息需要传递。例如,当一个 Button
向已登记的接收者通知自己被单击时,调用回调方法就可以了。定义不需要传递附加数据的事件时,可直接使用 EventArgs.Empty
,不用构造新的 EventArgs
对象。
# Step #2: Define the event member
An event member is defined using the C# keyword event. Each event member is given accessibility (which is almost always public so that other code can access the event member), a type of delegate indicating the prototype of the method(s) that will be called, and a name (which can be any valid identifier). Here is what the event member in our MailManager class looks like.
internal class MailManager { | |
// Step #2: Define the event member | |
public event EventHandler<NewMailEventArgs> NewMail; | |
... | |
} |
NewMail is the name of this event. The type of the event member is EventHandler, which means that all receivers of the event notification must supply a callback method whose prototype matches that of the EventHandler delegate type. Because the generic System.EventHandler delegate is defined as follows.
public delegate void EventHandler<TEventArgs>(Object sender, TEventArgs e); |
the method prototypes must look like the following.
void MethodName(Object sender, NewMailEventArgs e); |
💡注意:许多人奇怪事件模式为什么要求 sender
参数是 Object
类型。毕竟,只有 MailManager
才会引发传递了 NewMailEventArgs
对象的事件,所以回调方法更合适的原型似乎是下面这个:void MethodName(MailManager sender, NewMailEventArgs e);
要求 sender
是 Object
主要是因为继承。例如,假定 MailManager
成为 SmtpMailManager
的基类,那么回调方法的 sender
参数应该是 SmtpMailManager
类型而不是 MailManager
类型。但这不可能发生,因为 SmtpMailManager
继承了 NewMail
事件。所以,如果代码需要由 SmtpMailManager
引发事件,还是要将 sender
实参转型为 SmtpMailManager
。反正都要进行类型转换,这和将 sender
定为 Object
类型没什么两样。
将 sender
参数的类型定为 Object
的另一个原因是灵活性。它使委托能由多个类型使用,只要类型提供了一个会传递 NewMailEventArgs
对象的事件。例如,即使 PopMailManager
类不是从 MailManager
类派生的,也能使用这个委托。
此外,事件模式要求委托定义和回调方法将派生自 EventArgs
的参数命名为 e
。这个要求唯一的作用就是加强事件模式的一致性,使开发人员更容易学习和实现这个模式。注意,能自动生成源代码的工具 (比如 Microsoft Visual Studio) 也知道将参数命名为 e
。
最后,事件模式要求所有事件处理程序 <sup>①</sup > 的返回都是 void
。这很有必要,因为引发事件后可能要调用好几个回调方法,但没办法获得所有方法的返回值。将返回类型定为 void
,就不允许回调 (方法) 返回值。遗憾的是,FCL 中的一些事件处理程序 (比如 ResolveEventHandler
) 没有遵循 Microsoft 自定的模式。例如, ResolveEventHandler
事件处理程序会返回 Assembly
类型的对象。
# Step #3: Define a method responsible for raising the event to notify registered objects that the event has occurred
By convention, the class should define a protected, virtual method that is called by code internally within the class and its derived classes when the event is to be raised. This method takes one parameter, a NewMailEventArgs object, which includes the information passed to the objects receiving the notification. The default implementation of this method simply checks if any objects have registered interest in the event and, if so, the event will be raised, thereby notifying the registered methods that the event has occurred. Here is what the method in our MailManager class looks like.
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); | |
} | |
... | |
} |
以线程安全的方式引发事件
.NET Framework 刚发布时建议开发者用以下方式引发事件:
// 版本 1
protected virtual void OnNewMail(NewMailEventArgs e) {
if (NewNmil != 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
的方式对上述代码进行优化。如果发生这种情况,版本 2 就和版本 1 就没有任何区别。所以,人有可能抛出 NullReferenceException
异常。
要想整个修正这个问题,应该像下面这样重写 OnNewMail
:
// 版本 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
方法。
虽然最后一个版本很完美,是技术正确的版本,但版本 2 实际也是可以使用的,因为 JIT 编译器理解这个模式,知道自己不该将局部变量 temp
“优化” 掉。具体地说,MIcrosoft 的所有 JIT 编译器都 “尊重” 那些不会造成对堆内存的新的读取动作的不变量 (invariant)。所以,在局部变量中缓存一个引用,可确保堆引用只被访问一次。这一点并未在文档中反映,理论上说将来可能改变,这正是为什么应该使用最后一个版本的原因。但实际上,Microsoft 的 JIT 编译器永远没有可能真的进行修改来破坏这个模式,否则太多的应用程序都会 “遭殃”。此外,事件主要在单线程的情形 (WPF 和 Windows Store 应用) 中使用,所以线程安全不是问题。
还要注意,考虑到线程竞态条件,方法有可能在从事件的委托链中移除之后得到调用。
As a convenience, you could define an extension method (as discussed in Chapter 8, “Methods”) that encapsulates this thread-safety logic. Define the extension method as follows.
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); | |
} | |
} |
And now, we can rewrite the OnNewMail method as follows.
protected virtual void OnNewMail(NewMailEventArgs e) { | |
e.Raise(this, ref m_NewMail); | |
} |
A class that uses MailManager as a base type is free to override the OnNewMail method. This capability gives the derived class control over the raising of the event. The derived class can handle the new email message in any way it sees fit. Usually, a derived type calls the base type’s OnNewMail method so that the registered method(s) receive the notification. However, the derived class might decide to disallow the event from being forwarded.
# Step #4: Define a method that translates the input into the desired event
Your class must have some method that takes some input and translates it into the raising of the event. In my MailManager example, the SimulateNewMail method is called to indicate that a new email message has arrived into MailManager.
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); | |
} | |
} |
SimulateNewMail accepts information about the message and constructs a NewMailEventArgs object, passing the message information to its constructor. MailManager’s own virtual OnNewMail method is then called to formally notify the MailManager object of the new email message. Usually, this causes the event to be raised, notifying all of the registered methods. (As mentioned before, a class using MailManager as a base class can override this behavior.)
💡小结:在.NET 程序库中几乎所有和事件相关的定义中,都体现了一个标准的模式。该模式保证了程序库和用户代码使用事件的一致性。第一步:定义类型来容纳所有需要发送给事件通知接收者的附加信息。根据约定,这种类应该从 System.EventArgs
派生,而且类名以 EventArgs
结束。第二步:定义事件成员。事件成员使用 C# 关键字 event 定义。每个事件成员都要指定以下内容:可访问性标识符(几乎肯定是 public,这样其他代码才能访问该事件成员);委托类型,指出要调用的方法的原型;以及名称(可以是任何有效的标识符)。第三步:定义负责引发事件的方法来通知事件的登记对象。按照约定,类要定义一个受保护的虚方法。引发事件时,类及其派生类中的代码会调用该方法。方法只获取一个参数,即一个 EventArgs 的派生类,其中包含了传给接受通知的对象的消息。方法的默认实现只是检查一下是否有对象登记了对事件的关注。如果有,就引发事件来通知事件的登记对象。一般情况下,派生类会调用基类的 OnXXX 方法,使登记的方法能收到通知。但是,派生类也可以不允许事件转发。第四步:定义方法将输入转化为期望事件。
# How the Compiler Implements an Event
Now that you know how to define a class that offers an event member, let’s take a closer look at what an event really is and how it works. In the MailManager class, we have a line of code that defines the event member itself.
public event EventHandler<NewMailEventArgs> NewMail; |
When the C# compiler compiles the line above, it translates this single line of source code into the following three constructs.
// 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); | |
} |
The first construct is simply a field of the appropriate delegate type. This field is a reference to the head of a list of delegates that will be notified when this event occurs. This field is initialized to null, meaning that no listeners have registered interest in the event. When a method registers interest in the event, this field refers to an instance of the EventHandler delegate, which may refer to additional EventHandler delegates. When a listener registers interest in an event, the listener is simply adding an instance of the delegate type to the list. Obviously, unregistering means removing the delegate from the list.
You’ll notice that the delegate field, NewMail in this example, is always private even though the original line of source code defines the event as public. The reason for making the delegate field private is to prevent code outside the defining class from manipulating it improperly. If the field were public, any code could alter the value in the field and potentially wipe out all of the delegates that have registered interest in the event.
The second construct the C# compiler generates is a method that allows other objects to register their interest in the event. The C# compiler automatically names this function by prepending add_ to the event’s name (NewMail). The C# compiler automatically generates the code that is inside this method. The code always calls System.Delegate’s static Combine method, which adds the instance of a delegate to the list of delegates and returns the new head of the list, which gets saved back in the field.
The third construct the C# compiler generates is a method that allows an object to unregister its interest in the event. Again, the C# compiler automatically names this function by prepending remove_ to the event’s name (NewMail). The code inside this method always calls Delegate’s static Remove method, which removes the instance of a delegate from the list of delegates and returns the new head of the list, which gets saved back in the field.
💡警告:试图删除从未添加过的方法, Delegate
的 Remove
方法在内部不做任何事情。也就是说,不会抛出任何异常,也不会显示任何警告;事件的方法集合保持不变。
💡注意: add
和 remove
方法以线程安全的一种模式更新值。该模式的详情将在 29.3.4 节 “Interlocked Anything 模式” 讨论。
In this example, the add and remove methods are public. The reason they are public is that the original line of source code declared the event to be public. If the event had been declared protected, the add and remove methods generated by the compiler would also have been declared protected. So, when you define an event in a type, the accessibility of the event determines what code can register and unregister interest in the event, but only the type itself can ever access the delegate field directly. Event members can also be declared as static or virtual, in which case the add and remove methods generated by the compiler would be either static or virtual, respectively.
In addition to emitting the aforementioned three constructs, compilers also emit an event definition entry into the managed assembly’s metadata. This entry contains some flags and the underlying delegate type, and refers to the add and remove accessor methods. This information exists simply to draw an association between the abstract concept of an “event” and its accessor methods. Compilers and other tools can use this metadata, and this information can also be obtained by using the System.Reflection.EventInfo class. However, the CLR itself doesn’t use this metadata information and requires only the accessor methods at run time.
💡小结:定义了提供事件成员的类之后,C# 编译器编译时会把它转换成 3 个构造。第一个构造是具有恰当委托类型的字段。该字段是对一个委托列表的头部的引用。事件发生时会通知这个列表中的委托。字段初始化为 null,表明无侦听者(listener)登记对该事件的关注。侦听者登记对事件的关注时,只需将委托类型的一个实例添加到列表中。显然,注销(对事件的关注)意味着从列表中移除委托。注意,即使原始代码行将事件定义为 public,委托字段也始终是 private。将委托字段定义为 private,目的是防止类外部的代码不正确地操纵它。如果字段是 public,任何代码都能更改字段中的值,并可能删除已登记了对事件的关注的委托。C# 编译器生成的第二个构造是一个方法,允许其他对象登记对事件的关注。生成的代码总是调用 System.Delegate
的静态 Combine
方法,它将委托实例添加到委托列表中,返回新的列表头(地址),并将这个地址存回字段。C# 编译器生成的第三个构造也是一个方法,允许对象注销对事件的关注。方法中的代码总是调用 Delegate
的静态 Remove
方法,将委托实例从委托列表中删除,返回新的列表头(地址),并将这个地址存回字段。在类型中定义事件时,事件的可访问性决定了什么代码能登记和注销对事件的关注。但无论如何,只有类型本身才能直接访问委托字段。事件成员也可声明为 static 或 virtual。在这种情况下,编译器生成的 add 和 remove 方法分别标记为 static 或 virtual。除了生成上述 3 个构造,编译器还会在托管程序集的元数据中生成一个事件定义记录项。这个记录项包含了一些标志(flag)和基础委托类型(underlying delegate type),还引用了 add 和 remove 访问器方法。这些信息用于建立 “事件” 的抽象概念和它的访问器方法之间的联系。编译器和其他工具可利用这些元数据信息,并可通过 System.Reflection.EventInfo
类获取这些信息。但是,CLR 本身并不使用这些元数据信息,它在运行时只需要访问器方法。
# Designing a Type That Listens for an Event
The hard work is definitely behind you at this point. In this section, I’ll show you how to define a type that uses an event provided by another type. Let’s start off by examining the code for the Fax type.
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; | |
} | |
} |
When the email application initializes, it would first construct a MailManager object and save the reference to this object in a variable. Then the application would construct a Fax object, passing the reference to the MailManager object as a parameter. In the Fax constructor, the Fax object registers its interest in MailManager’s NewMail event using C#’s += operator.
mm.NewMail += FaxMsg; |
Because the C# compiler has built-in support for events, the compiler translates the use of the += operator into the following line of code to add the object’s interest in the event.
mm.add_NewMail(new EventHandler<NewMailEventArgs>(this.FaxMsg)); |
As you can see, the C# compiler is generating code that will construct an EventHandler delegate object that wraps the Fax class’s FaxMsg method. Then, the C# compiler calls the MailManager’s add_NewMail method, passing it the new delegate. Of course, you can verify all of this by compiling the code and looking at the IL with a tool such as ILDasm.exe.
Even if you’re using a programming language that doesn’t directly support events, you can still register a delegate with the event by calling the add accessor method explicitly. The effect is identical; the source code will just not look as pretty. It’s the add method that registers the delegate with the event by adding it to the event’s list of delegates.
When the MailManager object raises the event, the Fax object’s FaxMsg method gets called. The method is passed a reference to the MailManager object as the first parameter, sender. Most of the time, this parameter is ignored, but it can be used if the Fax object wants to access members of the MailManager object in response to the event notification. The second parameter is a reference to a NewMailEventArgs object. This object contains any additional information the designer of MailManager and NewMailEventArgs thought would be useful to the event receivers.
From the NewMailEventArgs object, the FaxMsg method has easy access to the message’s sender, the message’s recipient, and the message’s subject. In a real Fax object, this information would be faxed somewhere. In this example, the information is simply displayed in the console window.
When an object is no longer interested in receiving event notifications, it should unregister its interest. For example, the Fax object would unregister its interest in the NewMail event if the user no longer wanted his or her email forwarded to a fax. As long as an object has registered one of its methods with an event, the object can’t be garbage collected. If your type implements IDisposable’s Dispose method, the implementation should cause it to unregister interest in all events. (See Chapter 21, “The Managed Heap and Garbage Collection,” for more information about IDisposable.)
Code that demonstrates how to unregister for an event is shown in Fax’s Unregister method. This method is practically identical to the code shown in the Fax constructor. The only difference is that this code uses -= instead of +=. When the C# compiler sees code using the -= operator to unregister a delegate with an event, the compiler emits a call to the event’s remove method
mm.remove_NewMail(new EventHandler<NewMailEventArgs>(FaxMsg)); |
As with the += operator, even if you’re using a programming language that doesn’t directly support events, you can still unregister a delegate with the event by calling the remove accessor method explicitly. The remove method unregisters the delegate from the event by scanning the list for a delegate that wraps the same method as the one passed in. If a match is found, the existing delegate is removed from the event’s list of delegates. If a match isn’t found, no error occurs, and the list is unaltered.
By the way, C# requires your code to use the += and -= operators to add and remove delegates from the list. If you try to call the add or remove method explicitly, the C# compiler produces the CS0571 cannot explicitly call operator or accessor error message.
💡小结:C# 编译器内建了对事件的支持,会将 += 操作符翻译成具体代码来添加对象对事件的关注。即使使用的编程语言不直接支持事件,也可显式调用 add 访问器方法向事件登记委托。两者效果一样,最终都是用 add 访问器将委托添加到事件的委托列表中,从而完成委托向事件的登记。对象不再希望接收事件通知时,应注销对事件的关注。对象只要向事件登记了一个方法,便不能被垃圾回收。所以,如果你的类型要实现 IDisposable
和 Dispose
方法,就应该在实现中注销对所有事件的关注。和 += 操作符一样,即使编程语言不直接支持事件,也可显示调用 remove 访问器方法向事件注销委托。remove 方法为了向事件注销委托,需要扫描委托列表来寻找一个恰当的委托(其中包装的方法和传递的方法相同)。找到匹配,现有委托会从事件的委托列表中删除。没有找到也不会报错,列表不发生任何变动。C# 要求代码使用 += 和 -= 操作符在列表中增删委托。如果显式调用 add 和 remove 方法,C# 编译器会报告以下错误消息:CS0571:无法显式调用运算符或访问器。
# Explicitly Implementing an Event
The System.Windows.Forms.Control type defines about 70 events. If the Control type implemented the events by allowing the compiler to implicitly generate the add and remove accessor methods and delegate fields, every Control object would have 70 delegate fields in it just for the events! Because most programmers care about just a few events, an enormous amount of memory would be wasted for each object created from a Control-derived type. By the way, the ASP.NET System.Web.UI.Control and the Windows Presentation Foundation (WPF) System.Windows.UIElement type also offer many events that most programmers do not use.
In this section, I discuss how the C# compiler allows a class developer to explicitly implement an event, allowing the developer to control how the add and remove methods manipulate the callback delegates. I’m going to demonstrate how explicitly implementing an event can be used to efficiently implement a class that offers many events. However, there are certainly other scenarios where you might want to explicitly implement a type’s event.
To efficiently store event delegates, each object that exposes events will maintain a collection (usually a dictionary) with some sort of event identifier as the key and a delegate list as the value. When a new object is constructed, this collection is empty. When interest in an event is registered, the event’s identifier is looked up in the collection. If the event identifier is there, the new delegate is combined with the list of delegates for this event. If the event identifier isn’t in the collection, the event identifier is added with the delegate.
When the object needs to raise an event, the event identifier is looked up in the collection. If the collection doesn’t have an entry for the event identifier, nothing has registered interest in the event and no delegates need to be called back. If the event identifier is in the collection, the delegate list associated with the event identifier is invoked. Implementing this design pattern is the responsibility of the developer who is designing the type that defines the events; the developer using the type has no idea how the events are implemented internally.
Here is an example of how you could accomplish this pattern. First, I implemented an EventSet class that represents a collection of events and each event’s delegate list as follows.
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 }); | |
} | |
} |
💡Note : The FCL defines a type, System.Windows.EventHandlersStore, which does essentially the same thing as my EventSet class. Various WPF types use the EventHandlersStore type internally to maintain their sparse set of events. You’re certainly welcome to use the FCL’s EventHandlersStore type if you’d like. The big difference between the EventHandlersStore type and my EventSet type is that EventHandlersStore doesn’t offer any thread-safe way to access the events; you would have to implement your own thread-safe wrapper around the EventHandlersStore collection if you need to do this.
Now, I show a class that uses my EventSet class. This class has a field that refers to an EventSet object, and each of this class’s events is explicitly implemented so that each event’s add method stores the specified callback delegate in the EventSet object and each event’s remove method eliminates the specified callback delegate (if found).
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 | |
} |
Code that uses the TypeWithLotsOfEvents type can’t tell whether the events have been implemented implicitly by the compiler or explicitly by the developer. They just register the events by using normal syntax. Here is some code demonstrating this.
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..."); | |
} | |
} |
💡小结: System.Windows.Forms.Control
类型定义了大约 70 个事件。假如 Control 类型在实现事件时,允许编译器隐式生成 add 和 remove 访问器方法以及委托字段,那么每个 Control 对象仅为事件就要准备 70 个委托字段。由于大多数程序员只关心少数几个事件,所以每个从 Control
派生类型创建的对象都要浪费大量内存。因此,本节讨论了 C# 编译器如何如何允许类的开发人员显式实现一个事件,使开发人员能够控制 add 和 remove 方法处理回调委托的方法,并演示了如何通过显式实现事件来高效率地实现提供了大量事件的类。为了高效率存储事件委托,公开了事件的每个对象都要维护一个集合(通常时字典)。集合将某种形式的事件标识符作为键(key),将委托列表作为值(value)。使用 TypeWithLotsOfEvents
类型的代码不知道事件是由编译器隐式实现,还是由开发人员显式实现。它们只需要标准的语法向事件登记即可。