# 原則25:實現通知的事件模式
**By D.S.Qiu**
**尊重他人的勞動,支持原創,轉載請注明出處:[http://dsqiu.iteye.com](http://dsqiu.iteye.com)**
.NET 的事件模式無非就是觀察者模式的語法規范。(查看 Design Patterns, Gamma, Helm, Johnson, and Vlissides pp.293-303)事件定義類的通知消息。事件是構建在委托之上提供類型安全函數簽名的處理。事實上,大多數使用委托的例子就是事件,開發者會認為事件和委托是同一件事。在原則中,我給你介紹了使用委托而不是事件的用法。當你需要通知多個客戶告訴他們你的行為,你就可以觸發事件。事件就是對象通知觀察者。
考慮下面這個簡單例子。你正要應用中構建分發消息的日志類。它可以接受所有源頭的所有消息并且能夠分發給監聽者。這些監聽者可能是控制臺,或者是數據庫,或者是系統日志,或者是其他機制。你如下定義這個類,當消息到來就會觸發一個事件:
```
public class LoggerEventArgs : EventArgs
{
public string Message { get; private set; }
public int Priority { get; private set; }
public LoggerEventArgs(int p, string m)
{
Priority = p;
Message = m;
}
}
public class Logger
{
static Logger()
{
theOnly = new Logger();
}
private Logger()
{
}
private static Logger theOnly = null;
public static Logger Singleton
{
get { return theOnly; }
}
// Define the event:
public event EventHandler<LoggerEventArgs> Log;
// add a message, and log it.
public void AddMsg(int priority, string msg)
{
// This idiom discussed below.
EventHandler<LoggerEventArgs> l = Log;
if (l != null)
l(this, new LoggerEventArgs(priority, msg));
}
}
```
AddMsg 方法就是恰當觸發事件的方式。用臨時變量引用日志事件處理器能在多線程程序中保證共享條件安全。如果沒有復制引用,在 if 條件檢查和事件處理執行之間可以移除事件處理器。通過復制引用,就不會發生。
LoggerEventArgs 包含事件的優先級和消息。委托定義事件處理器。在 Logger 類中,使用 event 域定義事件處理器。編譯器檢查到 public event 域的定義會自動為你創建 add 和 remove 操作。和你下面的寫法產生的代碼是一樣的:
```
public class Logger
{
private EventHandler<LoggerEventArgs> log;
public event EventHandler<LoggerEventArgs> Log
{
add { log = log + value; }
remove { log = log - value; }
}
public void AddMsg(int priority, string msg)
{
EventHandler<LoggerEventArgs> l = log;
if (l != null)
l(null, new LoggerEventArgs(priority, msg));
}
}
```
C# 編譯器為 event 創建 add 和 remove 訪問器。我發現 public event 的聲明語句會比沒有 add/remove 語法更簡潔,更容易閱讀。所以當你聲明 public event 時,讓編譯器為你創建 add 和 remove 屬性。只有當你需要 add 和 remove 做更多事情才需要自己寫。
事件不需要掌握任何潛在的監聽者的信息。下面的類會自動路由所有消息到標準錯誤控制臺:
```
class ConsoleLogger
{
static ConsoleLogger()
{
Logger.Singleton.Log += (sender, msg) =>
{
Console.Error.WriteLine("{0}:\t{1}", msg.Priority.ToString(), msg.Message);
};
}
}
```
另一個類的實現是直接輸出到系統事件日志中:
```
class EventLogger
{
private static Logger logger = Logger.Singleton;
private static string eventSource;
private static EventLog logDest;
static EventLogger()
{
logger.Log += (sender, msg) =>
{
if (logDest != null)
logDest.WriteEntry(msg.Message,EventLogEntryType.Information, msg.Priority);
};
}
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;
}
}
}
```
當有消息產生,事件會通知任何監聽的客戶端。Logger 不需要關心哪些對象有監聽日志事件。
Logger 類只包含了一個事件。有些類(大多數窗口控制器)有大量的事件。在那些例子,為每個事件使用一個域是不可接受的。很多情況,在一個程序中只需要定義少量的事件。如果你遇到這種情況,你可以修改設計只需要在運行時創建的事件對象。
在核心框架的窗口控制子系統包含這樣處理的例子。怎么實現呢,添加一個子系統到 Logger 類。每個子系統就是一個事件。客戶端只會注冊事件到它相關的子系統中。
擴展的 Logger 類有一個 System.ComponentModel.EventHandlerList ,它存儲系統支持的所有事件對象。新的 AddMsg 添加 string 參數指定哪個子系統處理日志消息。如果子系統有監聽者,事件就會被觸發。同時,如果事件監聽者對所有消息感興趣,它的事件也會被觸發:
```
public sealed class Logger
{
private static System.ComponentModel.EventHandlerList Handlers = new EventHandlerList();
static public void AddLogger( string system, EventHandler<LoggerEventArgs> ev)
{
Handlers.AddHandler(system, ev);
}
static public void RemoveLogger(string system,EventHandler<LoggerEventArgs> ev)
{
Handlers.RemoveHandler(system, ev);
}
static public void AddMsg(string system, int priority, string msg)
{
if (!string.IsNullOrEmpty(system))
{
EventHandler<LoggerEventArgs> l = Handlers[system] as EventHandler<LoggerEventArgs>;
LoggerEventArgs args = new LoggerEventArgs( priority, msg);
if (l != null)
l(null, args);
// The empty string means receive all messages:
l = Handlers[""] as EventHandler<LoggerEventArgs>;
if (l != null)
l(null, args);
}
}
}
```
新的例子在 EventHandlerList 集合中存儲獨立的事件處理器。不好的是, EventHandlerList 還沒有泛型版本。所以,你在這個例子會看到很多比這本書其他地方要多的類型轉換。客戶端代碼注冊具體的子系統,一個新的事件對象就產生了。相同子系統都是檢索同一個事件對象。如果你類包含大量的事件接口,你就可以考慮使用事件處理器的集合。把事件對象成員的交給客戶端是否注冊事件處理器來決定。在 .NET 框架中,System.Windows.Forms.Control 類使用一個會隱藏所有的 event 域的更復雜變種的實現。每個事件都內部訪問集合添加和移除具體的處理器。你可以查看 C# 語言規范中了解更多這個語法習慣的細節。
EventHandler 類沒有更新到一個泛型版本。你不難用 Dictionary 構建一個自己的實現:
```
public sealed class Logger
{
private static Dictionary<string,
EventHandler<LoggerEventArgs>>
Handlers = new Dictionary<string,
EventHandler<LoggerEventArgs>>();
static public void AddLogger( string system, EventHandler<LoggerEventArgs> ev)
{
if (Handlers.ContainsKey(system))
Handlers[system] += ev;
else
Handlers.Add(system, ev);
}
static public void RemoveLogger(string system,EventHandler<LoggerEventArgs> ev)
{
// will throw exception if system
// does not contain a handler.
Handlers[system] -= ev;
}
static public void AddMsg(string system, int priority, string msg)
{
if (string.IsNullOrEmpty(system))
{
EventHandler<LoggerEventArgs> l = null;
Handlers.TryGetValue(system, out l);
LoggerEventArgs args = new LoggerEventArgs( priority, msg);
if (l != null)
l(null, args);
// The empty string means receive all messages:
l = Handlers[""] as EventHandler<LoggerEventArgs>;
if (l != null)
l(null, args);
}
}
}
```
泛型版本是增加代碼提供事件字典和類型轉換的權衡。我更喜歡泛型版本,但是它卻沒能夠權衡。
事件提供通知監聽者的標準語法。.NET 事件模式就是遵循 event 語法實現觀察者模式。任意數量的客戶注冊處理器到事件并處理它們。這些客戶不需要在編譯時被知道。事件不需要關心它的訂閱者就能正常工作。使用事件可以解耦通知的發送者和可能的接收者。發送者可以獨立于接收者開發。事件是廣播你的類發生的行為的消息的標準方式。
小結:
這個原則其實就是讓大家多用 event ,可以結構消息的發送者和接收者之間關系,有點老死不相往來卻彼此為了對方而活。
對于委托 delegate ,C# 衍生出了很多版本:deleage , Action , Func<> , Predicate<> 和 event 。
delegate: 就是原始的委托,可以理解為方法指針或方法的簽名
Action: 是沒有返回值的泛型委托
Func:有返回值的泛型委托
Predicate<>:返回值為 bool 類型的謂詞泛型委托
event:對 delegate 的封裝
至于,平常說的匿名函數和 Lambda 表達式跟具體函數一樣都是委托的實現方式。
特別地,這個還解決我之前看別人代碼的一個困苦:之前又看別人網絡層代碼老是會用一個臨時變量緩存(上文說的用臨時變量復制引用),一直不得其解。
歡迎各種不爽,各種噴,寫這個純屬個人愛好,秉持”分享“之德!
有關本書的其他章節翻譯請[點擊查看](/category/297763),轉載請注明出處,尊重原創!
如果您對D.S.Qiu有任何建議或意見可以在文章后面評論,或者發郵件(gd.s.qiu@gmail.com)交流,您的鼓勵和支持是我前進的動力,希望能有更多更好的分享。
轉載請在**文首**注明出處:[http://dsqiu.iteye.com/blog/2085830](/blog/2085830)
更多精彩請關注D.S.Qiu的博客和微博(ID:靜水逐風)
- 第一章 C# 語言習慣
- 原則1:使用 屬性(Poperty)代替可直接訪問的數據成員(Data Member)
- 原則2:偏愛 readonly 而不是 const
- 原則3:選擇 is 或 as 而不是強制類型轉換
- 原則4:使用條件特性(conditional attribute)代替 #if
- 原則5:總是提供 ToString()
- 原則6:理解幾個不同相等概念的關系
- 原則7:明白 GetHashCode() 的陷阱
- 原則8:優先考慮查詢語法(query syntax)而不是循環結構
- 原則9:在你的 API 中避免轉換操作
- 原則10:使用默認參數減少函數的重載
- 原則11:理解小函數的魅力
- 第二章 .NET 資源管理
- 原則12:選擇變量初始化語法(initializer)而不是賦值語句
- 原則13:使用恰當的方式對靜態成員進行初始化
- 原則14:減少重復的初始化邏輯
- 原則15:使用 using 和 try/finally 清理資源
- 原則16:避免創建不需要的對象
- 原則17:實現標準的 Dispose 模式
- 原則17:實現標準的 Dispose 模式
- 原則18:值類型和引用類型的區別
- 原則19:確保0是值類型的一個有效狀態
- 原則20:更傾向于使用不可變原子值類型
- 第三章 用 C# 表達設計
- 原則21:限制你的類型的可見性
- 原則22:選擇定義并實現接口,而不是基類
- 原則23:理解接口方法和虛函數的區別
- 原則24:使用委托來表達回調
- 原則25:實現通知的事件模式
- 原則26:避免返回類的內部對象的引用
- 原則27:總是使你的類型可序列化
- 原則28:創建大粒度的網絡服務 APIs
- 原則29:讓接口支持協變和逆變
- 第四章 和框架一起工作
- 原則30:選擇重載而不是事件處理器
- 原則31:用 IComparable&lt;T&gt; 和 IComparer&lt;T&gt; 實現排序關系
- 原則32:避免 ICloneable
- 原則33:只有基類更新處理才使用 new 修飾符
- 原則34:避免定義在基類的方法的重寫
- 原則35:理解 PLINQ 并行算法的實現
- 原則36:理解 I/O 受限制(Bound)操作 PLINQ 的使用
- 原則37:構造并行算法的異常考量
- 第五章 雜項討論
- 原則38:理解動態(Dynamic)的利與弊
- 原則39:使用動態對泛型類型參數的運行時類型的利用
- 原則40:使用動態接收匿名類型參數