Effective C# 原則22:用事件定義對外介面(譯)

來源:互聯網
上載者:User

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類不必預Crowdsourced Security Testing道任何對訊息感興趣的對象。

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.

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.