Effective C# 原则22:用事件定义对外接口
可以用事件给你的类型定义一些外部接口。事件是基于委托的,因为委托可以提供类型安全的函数签名到事件句柄上。加上大多数委托的例子都是使用事件来说明的,以至于开发人员一开始都认为委托与事件是一回事。在原则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#中事件可以减弱消息的发送者和可能的消息接受者之间的关系,发送者可以设计成与接受者无关。事件是类型把动作信息发布出去的标准方法。