.NET基礎拾遺(4)委託、事件、反射與特性
一、委託基礎
1.1 簡述委託的基本原理
委託這個概念對C++程式設計師來說並不陌生,因為它和C++中的函式指標非常類似,很多碼農也喜歡稱委託為安全的函式指標。無論這一說法是否正確,委託的的確確實現了和函式指標類似的功能,那就是提供了程式回撥指定方法的機制。
在委託內部,包含了一個指向某個方法的指標(這一點上委託實現機制和C++的函式指標一致),為何稱其為安全的呢?因此委託和其他.NET成員一樣是一種型別,任何委託物件都是繼承自System.Delegate的某個派生類的一個物件,下圖展示了在.NET中委託的類結構:
從上圖也可以看出,任何自定義的委託都繼承自基類System.Delegate
// 定義的一個委託 public delegate void TestDelegate(int i); public class Program { public static void Main(string[] args) { // 定義委託例項 TestDelegate td = new TestDelegate(PrintMessage); // 呼叫委託方法View Codetd(0); td.Invoke(1); Console.ReadKey(); } public static void PrintMessage(int i) { Console.WriteLine("這是第{0}個方法!", i.ToString()); } }
執行結果如下圖所示:
上述程式碼中定義了一個名為TestDelegate的新型別,該型別直接繼承自System.MulticastDelegate
需要注意的是,委託既可以接受例項方法,也可以接受靜態方法(如上述程式碼中接受的就是靜態方法),其區別我們在1.2中詳細道來。最後,委託被呼叫執行時,C#編譯器可以接收一種簡化程式設計師設計的語法,例如上述程式碼中的:td(1)。但是,本質上,委託的呼叫其實就是執行了在定義委託時所生成的Invoke方法。
1.2 委託回撥靜態方法和例項方法有何區別?
首先,我們知道靜態方法可以通過類名來訪問而無需任何例項物件,當然在靜態方法中也就不能訪問型別中任何非靜態成員。相反,例項方法則需要通過具體的例項物件來呼叫,可以訪問例項物件中的任何成員。
其次,當一個例項方法被呼叫時,需要通過例項物件來訪問,因此可以想象當繫結一個例項方法到委託時必須同時讓委託得到例項方法的程式碼段和例項物件的資訊,這樣在委託被回撥的時候.NET才能成功地執行該例項方法。
下圖展示了委託內部的主要結構:
① _target是一個指向目標例項的引用,當繫結一個例項方法給委託時,該引數會作為一個指標指向該方法所在型別的一個例項物件。相反,當繫結一個靜態方法時,該引數則被設定為null。
② _methodPtr則是一個指向繫結方法程式碼段的指標,這一點和C++的函式指標幾乎一致。繫結靜態方法或例項方法在這個成員的設定上並沒有什麼不同。
System.MulticastDelegate在內部結構上相較System.Delegate增加了一個重要的成員變數:_prev,它用於指向委託鏈中的下一個委託,這也是實現多播委託的基石。
1.3 神馬是鏈式委託?
鏈式委託也被稱為“多播委託”,其本質是一個由多個委託組成的連結串列。回顧上面1.2中的類結構,System.MulticastDelegate類便是為鏈式委託而設計的。當兩個及以上的委託被連結到一個委託鏈時,呼叫頭部的委託將導致該鏈上的所有委託方法都被執行。
下面看看在.NET中,如何申明一個鏈式委託:
// 定義的一個委託 public delegate void TestMulticastDelegate(); public class Program { public static void Main(string[] args) { // 申明委託並繫結第一個方法 TestMulticastDelegate tmd = new TestMulticastDelegate(PrintMessage1); // 繫結第二個方法 tmd += new TestMulticastDelegate(PrintMessage2); // 繫結第三個方法 tmd += new TestMulticastDelegate(PrintMessage3); // 呼叫委託 tmd(); Console.ReadKey(); } public static void PrintMessage1() { Console.WriteLine("呼叫第1個PrintMessage方法"); } public static void PrintMessage2() { Console.WriteLine("呼叫第2個PrintMessage方法"); } public static void PrintMessage3() { Console.WriteLine("呼叫第3個PrintMessage方法"); } }View Code
其執行結果如下圖所示:
可以看到,呼叫頭部的委託導致了所有委託方法的執行。通過前面的分析我們也可以知道:為委託+=增加方法以及為委託-=移除方法讓我們看起來像是委託被修改了,其實它們並沒有被修改。事實上,委託是恆定的。在為委託增加和移除方法時實際發生的是建立了一個新的委託,其呼叫列表是增加和移除後的方法結果。
另一方面,+= 或-= 這是一種簡單明瞭的寫法,回想在WindowsForm或者ASP.NET WebForms開發時,當新增一個按鈕事件,VS便會自動為我們生成類似的程式碼,這樣一想是不是又很熟悉了。
現在,我們再用一種更簡單明瞭的方法來寫:
TestMulticastDelegate tmd = PrintMessage1; tmd += PrintMessage2; tmd += PrintMessage3; tmd();View Code
其執行結果與上圖一致,只不過C#編譯器的智慧化已經可以幫我們省略了很多程式碼。
最後,我們要用一種比較複雜的方法來寫,但是卻是鏈式委託的核心所在:
TestMulticastDelegate tmd1 = new TestMulticastDelegate(PrintMessage1); TestMulticastDelegate tmd2 = new TestMulticastDelegate(PrintMessage2); TestMulticastDelegate tmd3 = new TestMulticastDelegate(PrintMessage3); // 核心本質:將三個委託串聯起來 TestMulticastDelegate tmd = tmd1 + tmd2 + tmd3; tmd.Invoke();View Code
我們在實際開發中經常使用第二種方法,但是卻不能不瞭解方法三,它是鏈式委託的本質所在。
1.4 鏈式委託的執行順序是怎麼樣的?
前面我們已經知道鏈式委託的基本特性就是一個以委託組成的連結串列,而當委託鏈上任何一個委託方法被呼叫時,其後面的所有委託方法都將會被依次地順序呼叫。那麼問題來了,委託鏈上的順序是如何形成的?這裡回顧一下上面1.3中的示例程式碼,通過Reflector反編譯一下,一探究竟:
從編譯後的結果可以看到,+=的本質又是呼叫了Delegate.Combine方法,該方法將兩個委託連結起來,並且把第一個委託放在第二個委託之前,因此可以將兩個委託的相加理解為Deletegate.Combine(Delegate a,Delegate b)的呼叫。我們可以再次回顧System.MulticastDelegate的類結構:
其中_prev成員是一個指向下一個委託成員的指標,當某個委託被連結到當前委託的後面時,該成員會被設定為指向那個後續的委託例項。.NET也是依靠這一個引用來逐一找到當前委託的所有後續委託並以此執行方法。
那麼,問題又來了?程式設計師能夠有能力控制鏈式委託的執行順序呢?也許我們會說,只要在定義時按照需求希望的順序來依次新增就可以了。但是,如果要在定義完成之後突然希望改變執行順序呢?又或者,程式需要按照實際的執行情況再來決定鏈式委託的執行順序呢?
接下來就是見證奇蹟的時刻:
// 申明委託並繫結第一個方法 TestMulticastDelegate tmd = new TestMulticastDelegate(PrintMessage1); // 繫結第二個方法 tmd += new TestMulticastDelegate(PrintMessage2); // 繫結第三個方法 tmd += new TestMulticastDelegate(PrintMessage3); // 獲取所有委託方法 Delegate[] dels = tmd.GetInvocationList();
上述程式碼呼叫了定義在System.MulticastDelegate中的GetInvocationList()方法,用以獲得整個鏈式委託中的所有委託。接下來,我們就可以按照我們所希望的順序去執行它們。
1.5 可否定義有返回值方法的委託鏈?
委託的方法既可以是無返回值的,也可以是有返回值的,但如果多一個帶返回值的方法被新增到委託鏈中時,我們需要手動地呼叫委託鏈上的每個方法,否則只能得到委託鏈上最後被呼叫的方法的返回值。
為了驗證結論,我們可以通過如下程式碼進行演示:
// 定義一個委託 public delegate string GetStringDelegate(); class Program { static void Main(string[] args) { // GetSelfDefinedString方法被最後新增 GetStringDelegate myDelegate1 = GetDateTimeString; myDelegate1 += GetTypeNameString; myDelegate1 += GetSelfDefinedString; Console.WriteLine(myDelegate1()); Console.WriteLine(); // GetDateTimeString方法被最後新增 GetStringDelegate myDelegate2 = GetSelfDefinedString; myDelegate2 += GetTypeNameString; myDelegate2 += GetDateTimeString; Console.WriteLine(myDelegate2()); Console.WriteLine(); // GetTypeNameString方法被最後新增 GetStringDelegate myDelegate3 = GetSelfDefinedString; myDelegate3 += GetDateTimeString; myDelegate3 += GetTypeNameString; Console.WriteLine(myDelegate3()); Console.ReadKey(); } static string GetDateTimeString() { return DateTime.Now.ToString(); } static string GetTypeNameString() { return typeof(Program).ToString(); } static string GetSelfDefinedString() { string result = "我是一個字串!"; return result; } }View Code
其執行結果如下圖所示:
從上圖可以看到,雖然委託鏈中的所有方法都被正確執行,但是我們只得到了最後一個方法的返回值。在這種情況下,我們應該如何得到所有方法的返回值呢?回顧剛剛提到的GetInvocationList()方法,我們可以利用它來手動地執行委託鏈中的每個方法。
GetStringDelegate myDelegate1 = GetDateTimeString; myDelegate1 += GetTypeNameString; myDelegate1 += GetSelfDefinedString; foreach (var del in myDelegate1.GetInvocationList()) { Console.WriteLine(del.DynamicInvoke()); }View Code
通過上述程式碼,委託鏈中每個方法的返回值都不會丟失,下圖是執行結果:
1.6 簡述委託的應用場合
委託的功能和其名字非常類似,在設計中其思想在於將工作委派給其他特定的型別、元件、方法或程式集。委託的使用者可以理解為工作的分派者,在通常情況下使用者清楚地知道哪些工作需要執行、執行的結果又是什麼,但是他不會親自地去做這些工作,而是恰當地把這些工作分派出去。
這裡,我們假設要寫一個日誌子系統,該子系統的需求是使用者希望的都是一個單一的方法傳入日誌內容和日誌型別,而日誌子系統會根據具體情況來進行寫日誌的動作。對於日誌子系統的設計者來說,寫一條日誌可能需要包含一系列的工作,而日誌子系統決定把這些工作進行適當的分派,這時就需要使用一個委託成員。
下面的程式碼展示了該日誌子系統的簡單實現方式:
① 定義列舉:日誌的類別
public enum LogType { Debug, Trace, Info, Warn, Error }View Code
② 定義委託,由日誌使用者直接執行來完成寫日誌的工作
public delegate void Log(string content, LogType type);
③ 定義日誌管理類,在構造方法中為記錄日誌委託定義了預設的邏輯(這裡採用了部分類的書寫,將各部分的委託方法分隔開,便於理解)
public sealed partial class LogManager:IDisposable { private Type _componentType; private String _logfile; private FileStream _fs; public Log WriteLog; //用來寫日誌的委託 //鎖 private static object mutext = new object(); //嚴格控制無參的構造方法 private LogManager() { WriteLog = new Log(PrepareLogFile); WriteLog += OpenStream; //開啟流 WriteLog += AppendLocalTime; //新增本地時間 WriteLog += AppendSeperator; //新增分隔符 WriteLog += AppendComponentType;//新增模組類別 WriteLog += AppendSeperator; //新增分隔符 WriteLog += AppendType; //新增日誌類別 WriteLog += AppendSeperator; //新增分隔符 WriteLog += AppendContent; //新增內容 WriteLog += AppendNewLine; //添加回車 WriteLog += CloseStream; //關閉流 } /// <summary> /// 構造方法 /// </summary> /// <param name="type">使用該日誌的型別</param> /// <param name="file">日誌檔案全路徑</param> public LogManager(Type type, String file):this() { _logfile = file; _componentType = type; } /// <summary> /// 釋放FileStream物件 /// </summary> public void Dispose() { if (_fs != null) _fs.Dispose(); GC.SuppressFinalize(this); } ~LogManager() { if (_fs != null) _fs.Dispose(); } } /// <summary> /// 委託鏈上的方法(和日誌檔案有關的操作) /// </summary> public sealed partial class LogManager:IDisposable { /// <summary> /// 如果日誌檔案不存在,則新建日誌檔案 /// </summary> private void PrepareLogFile(String content, LogType type) { //只允許單執行緒建立日誌檔案 lock(mutext) { if (!File.Exists(_logfile)) using (FileStream fs = File.Create(_logfile)) { } } } /// <summary> /// 開啟檔案流 /// </summary> private void OpenStream(String content, LogType type) { _fs = File.Open(_logfile, FileMode.Append); } /// <summary> /// 關閉檔案流 /// </summary> private void CloseStream(String content, LogType type) { _fs.Close(); _fs.Dispose(); } } /// <summary> /// 委託鏈上的方法(和日誌時間有關的操作) /// </summary> public sealed partial class LogManager : IDisposable { /// <summary> /// 為日誌添加當前UTC時間 /// </summary> private void AppendUTCTime(String content, LogType type) { String time=DateTime.Now.ToUniversalTime().ToString(); Byte[] con = Encoding.Default.GetBytes(time); _fs.Write(con, 0, con.Length); } /// <summary> /// 為日誌新增本地時間 /// </summary> private void AppendLocalTime(String content, LogType type) { String time = DateTime.Now.ToLocalTime().ToString(); Byte[] con = Encoding.Default.GetBytes(time); _fs.Write(con, 0, con.Length); } } /// <summary> /// 委託鏈上的方法(和日誌內容有關的操作) /// </summary> public sealed partial class LogManager : IDisposable { /// <summary> /// 新增日誌內容 /// </summary> private void AppendContent(String content, LogType type) { Byte[] con = Encoding.Default.GetBytes(content); _fs.Write(con, 0, con.Length); } /// <summary> /// 為日誌新增元件型別 /// </summary> private void AppendComponentType(String content, LogType type) { Byte[] con = Encoding.Default.GetBytes(_componentType.ToString()); _fs.Write(con, 0, con.Length); } /// <summary> /// 新增日誌型別 /// </summary> private void AppendType(String content, LogType type) { String typestring = String.Empty; switch (type) { case LogType.Debug: typestring = "Debug"; break; case LogType.Error: typestring = "Error"; break; case LogType.Info: typestring = "Info"; break; case LogType.Trace: typestring = "Trace"; break; case LogType.Warn: typestring = "Warn"; break; default: typestring = ""; break; } Byte[] con = Encoding.Default.GetBytes(typestring); _fs.Write(con, 0, con.Length); } } /// <summary> /// 委託鏈上的方法(和日誌的格式控制有關的操作) /// </summary> public sealed partial class LogManager : IDisposable { /// <summary> /// 新增分隔符 /// </summary> private void AppendSeperator(String content, LogType type) { Byte[] con = Encoding.Default.GetBytes(" | "); _fs.Write(con, 0, con.Length); } /// <summary> /// 新增換行符 /// </summary> private void AppendNewLine(String content, LogType type) { Byte[] con = Encoding.Default.GetBytes("\r\n"); _fs.Write(con, 0, con.Length); } } /// <summary> /// 修改所使用的時間型別 /// </summary> public sealed partial class LogManager : IDisposable { /// <summary> /// 設定使用UTC時間 /// </summary> public void UseUTCTime() { WriteLog = new Log(PrepareLogFile); WriteLog += OpenStream; WriteLog += AppendUTCTime; WriteLog += AppendSeperator; WriteLog += AppendComponentType; WriteLog += AppendSeperator; WriteLog += AppendType; WriteLog += AppendSeperator; WriteLog += AppendContent; WriteLog += AppendNewLine; WriteLog += CloseStream; } /// <summary> /// 設定使用本地時間 /// </summary> public void UseLocalTime() { WriteLog = new Log(PrepareLogFile); WriteLog += OpenStream; WriteLog += AppendLocalTime; WriteLog += AppendSeperator; WriteLog += AppendComponentType; WriteLog += AppendSeperator; WriteLog += AppendType; WriteLog += AppendSeperator; WriteLog += AppendContent; WriteLog += AppendNewLine; WriteLog += CloseStream; } }View Code
日誌管理類定義了一些列符合Log委託的方法,這些方法可以被新增到記錄日誌的委託物件之中,以構成整個日誌記錄的動作。在日後的擴充套件中,主要的工作也集中在新增新的符合Log委託定義的方法,並且將其新增到委託鏈上。
④ 在Main方法中呼叫LogManager的Log委託例項來寫日誌,LogManager只需要管理這個委託,負責分派任務即可。
class Program { static void Main(string[] args) { //使用日誌 using (LogManager logmanager = new LogManager(Type.GetType("LogSystem.Program"), "C:\\TestLog.txt")) { logmanager.WriteLog("新建了日誌", LogType.Debug); logmanager.WriteLog("寫資料", LogType.Debug); logmanager.UseUTCTime(); logmanager.WriteLog("現在是UTC時間", LogType.Debug); logmanager.UseLocalTime(); logmanager.WriteLog("回到本地時間", LogType.Debug); logmanager.WriteLog("發生錯誤", LogType.Error); logmanager.WriteLog("準備退出", LogType.Info); } Console.ReadKey(); } }View Code
程式碼中初始化委託成員的過程既是任務分派的過程,可以注意到LogManager的UseUTCTime和UseLocalTime方法都是被委託成員進行了重新的分配,也可以理解為任務的再分配。
下圖是上述程式碼的執行結果,將日誌資訊寫入了C:\TestLog.txt中:
二、事件基礎
事件這一名稱對於我們.NET碼農來說肯定不會陌生,各種技術框架例如WindowsForm、ASP.NET WebForm都會有事件這一名詞,並且所有的定義都基本相同。在.NET中,事件和委託在本質上並沒有太多的差異,實際環境下事件的運用卻比委託更加廣泛。
2.1 簡述事件的基本使用方法
在Microsoft的產品文件上這樣來定義的事件:事件是一種使物件或類能夠提供通知的成員。客戶端可以通過提供事件處理程式為相應的事件新增可執行程式碼。設計和使用事件的全過程大概包括以下幾個步驟:
下面我們來按照規範的步驟來展示一個通過控制檯輸出事件的使用示例:
① 定義一個控制檯事件ConsoleEvent的引數型別ConsoleEventArgs
/// <summary> /// 自定義一個事件引數型別 /// </summary> public class ConsoleEventArgs : EventArgs { // 控制檯輸出的訊息 private string message; public string Message { get { return message; } } public ConsoleEventArgs() : base() { this.message = string.Empty; } public ConsoleEventArgs(string message) : base() { this.message = message; } }View Code
② 定義一個控制檯事件的管理者,在其中定義了事件型別的私有成員ConsoleEvent,並定義了事件的傳送方法SendConsoleEvent
/// <summary> /// 管理控制檯,在輸出前傳送輸出事件 /// </summary> public class ConsoleManager { // 定義控制檯事件成員物件 public event EventHandler<ConsoleEventArgs> ConsoleEvent; /// <summary> /// 控制檯輸出 /// </summary> public void ConsoleOutput(string message) { // 傳送事件 ConsoleEventArgs args = new ConsoleEventArgs(message); SendConsoleEvent(args); // 輸出訊息 Console.WriteLine(message); } /// <summary> /// 負責傳送事件 /// </summary> /// <param name="args">事件的引數</param> protected virtual void SendConsoleEvent(ConsoleEventArgs args) { // 定義一個臨時的引用變數,確保多執行緒訪問時不會發生問題 EventHandler<ConsoleEventArgs> temp = ConsoleEvent; if (temp != null) { temp(this, args); } } }View Code
③ 定義了事件的訂閱者Log,在其中通過控制檯時間的管理類公開的事件成員訂閱其輸出事件ConsoleEvent
/// <summary> /// 日誌型別,負責訂閱控制檯輸出事件 /// </summary> public class Log { // 日誌檔案 private const string logFile = @"C:\TestLog.txt"; public Log(ConsoleManager cm) { // 訂閱控制檯輸出事件 cm.ConsoleEvent += this.WriteLog; } /// <summary> /// 事件處理方法,注意引數固定模式 /// </summary> /// <param name="sender">事件的傳送者</param> /// <param name="args">事件的引數</param> private void WriteLog(object sender, EventArgs args) { // 檔案不存在的話則建立新檔案 if (!File.Exists(logFile)) { using (FileStream fs = File.Create(logFile)) { } } FileInfo fi = new FileInfo(logFile); using (StreamWriter sw = fi.AppendText()) { ConsoleEventArgs