Effective C# 原则22:用事件定义对外接口

JerryXia 发表于 , 阅读 (2,214)

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

添加新评论