Effective C# 原则22:用事件定义对外接口
Item 22: Define Outgoing Interfaces with Events
可以用事件给你的类型定义一些外部接口。事件是基于委托的,因为委托可以提供类型安全的函数签名到事件句柄上。加上大多数委托的例子都是使用事件来说明的,以至于开发人员一开始都认为委托与事件是一回事。在原则21里,我已经展示了一些不在事件上使用委托的例子。在你的类型与其它多个客户进行通信时,为了完成它们的行为,你必须引发事件。
一个简单的例子,你正在做一个日志类,就像一个信息发布机一样在应用程序里发布所有的消息。它接受所有从程序源发布的消息,并且把这些消息发布到感兴趣的听众那里。这些听众可以是控制台,数据库,系统日志,或者是其它的机制。就可以定义一个像下面这样的类,当消息到达时来引发事件:
public class LoggerEventArgs : EventArgs
{
public readonly string Message;
public readonly int Priority;
public LoggerEventArgs ( int p, string m )
{
Priority = p;
Message = m;
}
}
// Define the signature for the event handler:
public delegate void AddMessageEventHandler( object sender,
LoggerEventArgs msg );
public class Logger
{
static Logger( )
{
_theOnly = new Logger( );
}
private Logger( )
{
}
private static Logger _theOnly = null;
public Logger Singleton
{
get
{
return _theOnly;
}
}
// Define the event:
public event AddMessageEventHandler Log;
// add a message, and log it.
public void AddMsg ( int priority, string msg )
{
// This idiom discussed below.
AddMessageEventHandler l = Log;
if ( l != null )
l ( null, new LoggerEventArgs( priority, msg ) );
}
}
AddMsg方法演示了一个恰当的方法来引发事件。临时的日志句柄变量 是很重要的,它可以确保在各种多线程的情况下,日志句柄也是安全的。如果没有这个引用的COPY,用户就有可能在if检测语句和正式执行事件句柄之间移除事件句柄。有了引用COPY,这样的事情就不会发生了。
我还定义了一个LoggerEventArgs来保存事件和消息的优先级。委托定义了事件句柄的签名。而在Logger类的内部,事件字段定义了事件的句柄。编译器会认为事件是公共的字段,而且会为你添加Add和Remove两个操作。生成的代码与你这样手写的是一样的:
public class Logger
{
private AddMessageEventHandler _Log;
public event AddMessageEventHandler Log
{
add
{
_Log = _Log + value;
}
remove
{
_Log = _Log - value;
}
}
public void AddMsg (int priority, string msg)
{
AddMessageEventHandler l = _Log;
if (l != null)
l (null, new LoggerEventArgs (priority, msg));
}
}
}
C#编译器创建Add和Remove操作来访问事件。看到了吗,公共的事件定义语言很简洁,易于阅读和维护,而且更准确。当你在类中添加一个事件时,你就让编译器可以创建添加和移除属性。你可以,而且也应该,在有原则要强制添加时自己手动的写这些句柄。
事件不必知道可能成为监听者的任何资料,下面这个类自动把所有的消息发送到标准的错误设备(控制台)上:
class ConsoleLogger
{
static ConsoleLogger()
{
logger.Log += new AddMessageEventHandler( Logger_Log );
}
private static void Logger_Log( object sender,
LoggerEventArgs msg )
{
Console.Error.WriteLine( "{0}:\t{1}",
msg.Priority.ToString(),
msg.Message );
}
}
另一个类可以直接输出到系统事件日志:
class EventLogger
{
private static string eventSource;
private static EventLog logDest;
static EventLogger()
{
logger.Log +=new AddMessageEventHandler( Event_Log );
}
public static string EventSource
{
get
{
return eventSource;
}
set
{
eventSource = value;
if ( ! EventLog.SourceExists( eventSource ) )
EventLog.CreateEventSource( eventSource,
"ApplicationEventLogger" );
if ( logDest != null )
logDest.Dispose( );
logDest = new EventLog( );
logDest.Source = eventSource;
}
}
private static void Event_Log( object sender,
LoggerEventArgs msg )
{
if ( logDest != null )
logDest.WriteEntry( msg.Message,
EventLogEntryType.Information,
msg.Priority );
}
}
事件会在发生一些事情时,通知任意多个对消息感兴趣的客户。Logger类不必预先知道任何对消息感兴趣的对象。
Logger类只包含一个事件。大多数windows控件有很多事件,在这种情况下,为每一个事件添加一个字段并不是一个可以接受的方法。在某些情况下,一个程序中只实际上只定义了少量的事件。当你遇到这种情况时,你可以修改设计,只有在运行时须要事件时在创建它。
(译注:作者的一个明显相思就是,当他想说什么好时,就决不会,或者很少说这个事情的负面影响。其实事件对性能的影响是很大的,应该尽量少用。事件给我们带来的好处是很多的,但不要海滥用事件。作者在这里没有明说事件的负面影响。)
扩展的Logger类有一个System.ComponentModel.EventHandlerList容器,它存储了在给定系统中应该引发的事件对象。更新的AddMsg()方法现在带一个参数,它可以详细的指示子系统日志的消息。如果子系统有任何的监听者,事件就被引发。同样,如果事件的监听者在所有感兴趣的消息上监听,它同样会被引发:
public class Logger
{
private static System.ComponentModel.EventHandlerList
Handlers = new System.ComponentModel.EventHandlerList();
static public void AddLogger(
string system, AddMessageEventHandler ev )
{
Handlers[ system ] = ev;
}
static public void RemoveLogger( string system )
{
Handlers[ system ] = null;
}
static public void AddMsg ( string system,
int priority, string msg )
{
if ( ( system != null ) && ( system.Length > 0 ) )
{
AddMessageEventHandler l =
Handlers[ system ] as AddMessageEventHandler;
LoggerEventArgs args = new LoggerEventArgs(
priority, msg );
if ( l != null )
l ( null, args );
// The empty string means receive all messages:
l = Handlers[ "" ] as AddMessageEventHandler;
if ( l != null )
l( null, args );
}
}
}
这个新的例子在Event HandlerList集合中存储了个别的事件句柄,客户代码添加到特殊的子系统中,而且新的事件对象被创建。然后同样的子系统需要时,取回同样的事件对象。如果你开发一个类包含有大量的事件实例,你应该考虑使用事件句柄集合。当客户附加事件句柄时,你可以选择创建事件成员。在.Net框架内部,System.Windows.Forms.Control类对事件使用了一个复杂且变向的实现,从而隐藏了复杂的事件成员字段。每一个事件字段在内部是通过访问集合来添加和移除实际的句柄。关于C#语言的这一特殊习惯,你可以在原则49中发现更多的信息。
你用事件在类上定义了一个外接的接口:任意数量的客户可以添加句柄到事件上,而且处理它们。这些对象在编译时不必知道是谁。事件系统也不必知道详细就可以合理的使用它们。在C#中事件可以减弱消息的发送者和可能的消息接受者之间的关系,发送者可以设计成与接受者无关。事件是类型把动作信息发布出去的标准方法。
===================================
Item 22: Define Outgoing Interfaces with Events
Events define the outgoing interface for your type. Events are built on delegates to provide type-safe function signatures for event handlers. Add to this the fact that most examples that use delegates are events, and developers start thinking that events and delegates are the same things. In Item 21, I showed you examples of when you can use delegates without defining events. You should raise events when your type must communicate with multiple clients to inform them of actions in the system.
Consider a simple example. You're building a log class that acts as a dispatcher of all messages in an application. It will accept all messages from sources in your application and will dispatch those messages to any interested listeners. These listeners might be attached to the console, a database, the system log, or some other mechanism. You define the class as follows, to raise one event whenever a message arrives:
public class LoggerEventArgs : EventArgs
{
public readonly string Message;
public readonly int Priority;
public LoggerEventArgs ( int p, string m )
{
Priority = p;
Message = m;
}
}
// Define the signature for the event handler:
public delegate void AddMessageEventHandler( object sender,
LoggerEventArgs msg );
public class Logger
{
static Logger( )
{
_theOnly = new Logger( );
}
private Logger( )
{
}
private static Logger _theOnly = null;
public Logger Singleton
{
get
{
return _theOnly;
}
}
// Define the event:
public event AddMessageEventHandler Log;
// add a message, and log it.
public void AddMsg ( int priority, string msg )
{
// This idiom discussed below.
AddMessageEventHandler l = Log;
if ( l != null )
l ( null, new LoggerEventArgs( priority, msg ) );
}
}
The AddMsg method showsthe proper way to raise events. The temporary variable to reference the log event handler is an important safeguard against race conditions in multithreaded programs. Without the copy of the reference, clients could remove event handlers between the if statement check and the execution of the event handler. By copying the reference, that can't happen.
I've defined LoggerEventArgs to hold the priority of an event and the message. The delegate defines the signature for the event handler. Inside the Logger class, the event field defines the event handler. The compiler sees the public event field definition and creates the Add and Remove operators for you. The generated code is exactly the same as though you had written the following:
public class Logger
{
private AddMessageEventHandler _Log;
public event AddMessageEventHandler Log
{
add
{
_Log = _Log + value;
}
remove
{
_Log = _Log - value;
}
}
public void AddMsg (int priority, string msg)
{
AddMessageEventHandler l = _Log;
if (l != null)
l (null, new LoggerEventArgs (priority, msg));
}
}
}
The C# compiler creates the add and remove accessors for the event. I find the public event declaration language more concise, easier to read and maintain, and more correct. When you create events in your class, declare public events and let the compiler create the add and remove properties for you. You can and should write these handlers yourself when you have additional rules to enforce.
Events do not need to have any knowledge about the potential listeners. The following class automatically routes all messages to the Standard Error console:
class ConsoleLogger
{
static ConsoleLogger()
{
logger.Log += new AddMessageEventHandler( Logger_Log );
}
private static void Logger_Log( object sender,
LoggerEventArgs msg )
{
Console.Error.WriteLine( "{0}:\t{1}",
msg.Priority.ToString(),
msg.Message );
}
}
Another class could direct output to the system event log:
class EventLogger
{
private static string eventSource;
private static EventLog logDest;
static EventLogger()
{
logger.Log +=new AddMessageEventHandler( Event_Log );
}
public static string EventSource
{
get
{
return eventSource;
}
set
{
eventSource = value;
if ( ! EventLog.SourceExists( eventSource ) )
EventLog.CreateEventSource( eventSource,
"ApplicationEventLogger" );
if ( logDest != null )
logDest.Dispose( );
logDest = new EventLog( );
logDest.Source = eventSource;
}
}
private static void Event_Log( object sender,
LoggerEventArgs msg )
{
if ( logDest != null )
logDest.WriteEntry( msg.Message,
EventLogEntryType.Information,
msg.Priority );
}
}
Events notify any number of interested clients that something happened. The Logger class does not need any prior knowledge of which objects are interested in logging events.
The Logger class contained only one event. There are classes (mostly Windows controls) that have very large numbers of events. In those cases, the idea of using one field per event might be unacceptable. In some cases, only a small number of the defined events is actually used in any one application. When you encounter that situation, you can modify the design to create the event objects only when needed at runtime.
The core framework contains examples of how to do this in the Windows control subsystem. To show you how, add subsystems to the Logger class. You create an event for each subsystem. Clients register on the event that is pertinent to their subsystem.
The extended Logger class has a System.ComponentModel.EventHandlerList container that stores all the event objects that should be raised for a given system. The updated AddMsg() method now takes a string parameter that specifies the subsystem generating the log message. If the subsystem has any listeners, the event gets raised. Also, if an event listener has registered an interest in all messages, its event gets raised:
public class Logger
{
private static System.ComponentModel.EventHandlerList
Handlers = new System.ComponentModel.EventHandlerList();
static public void AddLogger(
string system, AddMessageEventHandler ev )
{
Handlers[ system ] = ev;
}
static public void RemoveLogger( string system )
{
Handlers[ system ] = null;
}
static public void AddMsg ( string system,
int priority, string msg )
{
if ( ( system != null ) && ( system.Length > 0 ) )
{
AddMessageEventHandler l =
Handlers[ system ] as AddMessageEventHandler;
LoggerEventArgs args = new LoggerEventArgs(
priority, msg );
if ( l != null )
l ( null, args );
// The empty string means receive all messages:
l = Handlers[ "" ] as AddMessageEventHandler;
if ( l != null )
l( null, args );
}
}
}
This new example stores the individual event handlers in the Event HandlerList collection. Client code attaches to a specific subsystem, and a new event object is created. Subsequent requests for the same subsystem retrieve the same event object. If you develop a class that contains a large number of events in its interface, you should consider using this collection of event handlers. You create event members when clients attach to the event handler on their choice. Inside the .NET Framework, the System.Windows.Forms.Control class uses a more complicated variation of this implementation to hide the complexity of all its event fields. Each event field internally accesses a collection of objects to add and remove the particular handlers. You can find more information that shows this idiom in the C# language specification (see Item 49).
You define outgoing interfaces in classes with events: Any number of clients can attach handlers to the events and process them. Those clients need not be known at compile time. Events don't need subscribers for the system to function properly. Using events in C# decouples the sender and the possible receivers of notifications. The sender can be developed completely independently of any receivers. Events are the standard way to broadcast information about actions that your type has taken.