.NET基礎之委託、事件、反射與特性
轉自:http://www.cnblogs.com/edisonchou/p/4827578.html
委託、事件、反射與特性
Index :
(3)字串、集合與流
(4)委託、事件、反射與特性
(5)多執行緒開發基礎
一、委託基礎
1.1 簡述委託的基本原理
委託這個概念對C++程式設計師來說並不陌生,因為它和C++中的函式指標非常類似,很多碼農也喜歡稱委託為安全的函式指標
在委託內部,包含了一個指向某個方法的指標(這一點上委託實現機制和C++的函式指標一致),為何稱其為安全的呢?因此委託和其他.NET成員一樣是一種型別,任何委託物件都是繼承自System.Delegate的某個派生類的一個物件,下圖展示了在.NET中委託的類結構:
從上圖也可以看出,任何自定義的委託都繼承自基類System.Delegate,在這個類中,定義了大部分委託的特性。那麼,下面可以看看在.NET中如何使用委託:
// 定義的一個委託 public delegate void TestDelegate(int i); public class Program { public static void Main(string[] args) { // 定義委託例項 TestDelegate td = new TestDelegate(PrintMessage); // 呼叫委託方法 td(0); td.Invoke(1); Console.ReadKey(); } public static void PrintMessage(int i) { Console.WriteLine("這是第{0}個方法!", i.ToString()); } }
執行結果如下圖所示:
上述程式碼中定義了一個名為TestDelegate的新型別,該型別直接繼承自System.MulticastDelegate,而且其中會包含一個名為Invoke、BeginInvoke和EndInvoke的方法,這些步驟都是由C#編譯器自動幫我們完成的,可以通過Reflector驗證一下如下圖所示:
需要注意的是,委託既可以接受例項方法,也可以接受靜態方法(如上述程式碼中接受的就是靜態方法),其區別我們在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方法"); } }
其執行結果如下圖所示:
可以看到,呼叫頭部的委託導致了所有委託方法的執行。通過前面的分析我們也可以知道:為委託+=增加方法以及為委託-=移除方法讓我們看起來像是委託被修改了,其實它們並沒有被修改。事實上,委託是恆定的。在為委託增加和移除方法時實際發生的是建立了一個新的委託,其呼叫列表是增加和移除後的方法結果。
另一方面,+= 或-= 這是一種簡單明瞭的寫法,回想在WindowsForm或者ASP.NET WebForms開發時,當新增一個按鈕事件,VS便會自動為我們生成類似的程式碼,這樣一想是不是又很熟悉了。
現在,我們再用一種更簡單明瞭的方法來寫:
TestMulticastDelegate tmd = PrintMessage1; tmd += PrintMessage2; tmd += PrintMessage3; tmd();
其執行結果與上圖一致,只不過C#編譯器的智慧化已經可以幫我們省略了很多程式碼。
最後,我們要用一種比較複雜的方法來寫,但是卻是鏈式委託的核心所在:
TestMulticastDelegate tmd1 = new TestMulticastDelegate(PrintMessage1); TestMulticastDelegate tmd2 = new TestMulticastDelegate(PrintMessage2); TestMulticastDelegate tmd3 = new TestMulticastDelegate(PrintMessage3); // 核心本質:將三個委託串聯起來 TestMulticastDelegate tmd = tmd1 + tmd2 + tmd3; tmd.Invoke();
我們在實際開發中經常使用第二種方法,但是卻不能不瞭解方法三,它是鏈式委託的本質所在。
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; } }
其執行結果如下圖所示:
從上圖可以看到,雖然委託鏈中的所有方法都被正確執行,但是我們只得到了最後一個方法的返回值。在這種情況下,我們應該如何得到所有方法的返回值呢?回顧剛剛提到的GetInvocationList()方法,我們可以利用它來手動地執行委託鏈中的每個方法。
GetStringDelegate myDelegate1 = GetDateTimeString; myDelegate1 += GetTypeNameString; myDelegate1 += GetSelfDefinedString; foreach (var del in myDelegate1.GetInvocationList()) { Console.WriteLine(del.DynamicInvoke()); }
通過上述程式碼,委託鏈中每個方法的返回值都不會丟失,下圖是執行結果:
1.6 簡述委託的應用場合
委託的功能和其名字非常類似,在設計中其思想在於將工作委派給其他特定的型別、元件、方法或程式集。委託的使用者可以理解為工作的分派者,在通常情況下使用者清楚地知道哪些工作需要執行、執行的結果又是什麼,但是他不會親自地去做這些工作,而是恰當地把這些工作分派出去。
這裡,我們假設要寫一個日誌子系統,該子系統的需求是使用者希望的都是一個單一的方法傳入日誌內容和日誌型別,而日誌子系統會根據具體情況來進行寫日誌的動作。對於日誌子系統的設計者來說,寫一條日誌可能需要包含一系列的工作,而日誌子系統決定把這些工作進行適當的分派,這時就需要使用一個委託成員。
下面的程式碼展示了該日誌子系統的簡單實現方式:
① 定義列舉:日誌的類別
public enum LogType { Debug, Trace, Info, Warn, Error }
② 定義委託,由日誌使用者直接執行來完成寫日誌的工作
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; } }
日誌管理類定義了一些列符合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(); } }
程式碼中初始化委託成員的過程既是任務分派的過程,可以注意到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; } }
② 定義一個控制檯事件的管理者,在其中定義了事件型別的私有成員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); } } }
③ 定義了事件的訂閱者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 cea = args as ConsoleEventArgs; sw.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + "|" + sender.ToString() + "|" + cea.Message); } } }
④ 在Main方法中進行測試:
class Program { static void Main(string[] args) { // 控制檯事件管理者 ConsoleManager cm = new ConsoleManager(); // 控制檯事件訂閱者 Log log = new Log(cm); cm.ConsoleOutput("測試控制檯輸出事件"); cm.ConsoleOutput("測試控制檯輸出事件"); cm.ConsoleOutput("測試控制檯輸出事件"); Console.ReadKey(); } }
當該程式執行時,ConsoleManager負責在控制檯輸出測試的字串訊息,與此同時,訂閱了控制檯輸出事件的Log類物件會在指定的日誌檔案中寫入這些字串訊息。可以看出,這是一個典型的觀察者模式的應用,也可以說事件為觀察者模式提供了便利的實現基礎。
2.2 事件和委託有神馬聯絡?
事件的定義和使用方式與委託極其類似,那麼二者又是何關係呢?經常聽人說,委託本質是一個型別,而事件本質是一個特殊的委託型別的例項。關於這個解釋,最好的辦法莫過於通過檢視原始碼和編譯後的IL程式碼進行分析。
① 回顧剛剛的程式碼,在ConsoleManager類中定義了一個事件成員
public event EventHandler<ConsoleEventArgs> ConsoleEvent;
EventHandler是.NET框架中提供的一種標準的事件模式,它是一個特殊的泛型委託型別,通過檢視元資料可以驗證這一點:
[Serializable] public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e);
正如上面程式碼所示,我們定義一個事件時,實際上是定義了一個特定的委託成員例項。該委託沒有返回值,並且有兩個引數:一個事件源和一個事件引數。而當事件的使用者訂閱該事件時,其本質就是將事件的處理方法加入到委託鏈之中。
② 下面通過Reflector來檢視一下事件ConsoleEvent的IL程式碼(中間程式碼),可以更方便地看到這一點:
首先,檢視EventHandler的IL程式碼,可以看到在C#編譯器編譯delegate程式碼時,編譯後是成為了一個class。
其次,當C#編譯器編譯event程式碼時,會首先為型別新增一個EventHandler<T>的委託例項物件,然後為其增加一對add/remove方法用來實現從委託鏈中新增和移除方法的功能。
通過檢視add_ConsoleEvent的IL程式碼,可以清楚地看到訂閱事件的本質是呼叫Delegate的Combine方法將事件處理方法繫結到委託鏈中。
L_0000: ldarg.0 L_0001: ldfld class [mscorlib]System.EventHandler`1<class ConsoleEventDemo.ConsoleEventArgs> ConsoleEventDemo.ConsoleManager::ConsoleEvent L_0006: stloc.0 L_0007: ldloc.0 L_0008: stloc.1 L_0009: ldloc.1 L_000a: ldarg.1 L_000b: call class [mscorlib]System.Delegate [mscorlib]System.Delegate::Combine(class [mscorlib]System.Delegate, class [mscorlib]System.Delegate) L_0010: castclass [mscorlib]System.EventHandler`1<class ConsoleEventDemo.ConsoleEventArgs> L_0015: stloc.2 L_0016: ldarg.0 L_0017: ldflda class [mscorlib]System.EventHandler`1<class ConsoleEventDemo.ConsoleEventArgs> ConsoleEventDemo.ConsoleManager::ConsoleEvent
Summary:事件是一個特殊的委託例項,提供了兩個供訂閱事件和取消訂閱的方法:add_event和remove_event,其本質都是基於委託鏈來實現。
2.3 如何設計一個帶有很多事件的型別?
多事件的型別在實際應用中並不少見,尤其是在一些使用者介面的型別中(例如在WindowsForm中的各種控制元件)。這些型別動輒將包含數十個事件,如果為每一個事件都新增一個事件成員,將導致無論使用者是否用到所有事件,每個型別物件都將佔有很大的記憶體,那麼對於系統的效能影響將不言而喻。事實上,.NET的開發小組運用了一種比較巧妙的方式來避免這一困境。
Solution:當某個型別具有相對較多的事件時,我們可以考慮顯示地設計訂閱、取消訂閱事件的方法,並且把所有的委託連結串列儲存在一個集合之中。這樣做就能避免在型別中定義大量的委託成員而導致型別過大。
下面通過一個具體的例項來說明這一設計:
① 定義包含大量事件的型別之一:使用EventHandlerList成員來儲存所有事件
public partial class MultiEventClass { // EventHandlerList包含了一個委託連結串列的容器,實現了多事件存放在一個容器之中的包裝,它使用的是連結串列資料結構 private EventHandlerList events; public MultiEventClass() { // 初始化EventHandlerList events = new EventHandlerList(); } // 釋放EventHandlerList public void Dispose() { events.Dispose(); } }
② 定義包含大量事件的型別之二:申明多個具體的事件
public partial class MultiEventClass { #region event1 // 事件1的委託原型 public delegate void Event1Handler(object sender, EventArgs e); // 事件1的靜態Key protected static readonly object Event1Key = new object(); // 訂閱事件和取消訂閱 // 注意:EventHandlerList並不提供執行緒同步,所以加上執行緒同步屬性 public event Event1Handler Event1 { [MethodImpl(MethodImplOptions.Synchronized)] add { events.AddHandler(Event1Key, value); } [MethodImpl(MethodImplOptions.Synchronized)] remove { events.RemoveHandler(Event1Key, value); } } // 觸發事件1 protected virtual void OnEvent1(EventArgs e) { events[Event1Key].DynamicInvoke(this, e); } // 簡單地觸發事件1,以便於測試 public void RiseEvent1() { OnEvent1(EventArgs.Empty); } #endregion #region event2 // 事件2的委託原型 public delegate void Event2Handler(object sender, EventArgs e); // 事件2的靜態Key protected static readonly object Event2Key = new object(); // 訂閱事件和取消訂閱 // 注意:EventHandlerList並不提供執行緒同步,所以加上執行緒同步屬性 public event Event2Handler Event2 { [MethodImpl(MethodImplOptions.Synchronized)] add { events.AddHandler(Event2Key, value); } [MethodImpl(MethodImplOptions.Synchronized)] remove { events.RemoveHandler(Event2Key, value); } } // 觸發事件2 protected virtual void OnEvent2(EventArgs e) { events[Event2Key].DynamicInvoke(this, e); } // 簡單地觸發事件2,以便於測試 public void RiseEvent2() { OnEvent2(EventArgs.Empty); } #endregion }
③ 定義事件的訂閱者(它對多事件型別內部的構造一無所知)
public class Customer { public Customer(MultiEventClass events) { // 訂閱事件1 events.Event1 += Event1Handler; // 訂閱事件2 events.Event2 += Event2Handler; } // 事件1的回撥方法 private void Event1Handler(object sender, EventArgs e) { Console.WriteLine("事件1被觸發"); } // 事件2的回撥方法 private void Event2Handler(object sender, EventArgs e) { Console.WriteLine("事件2被觸發"); } }
④ 編寫入口方法來測試多事件的觸發
class Program { static void Main(string[] args) { using(MultiEventClass mec = new MultiEventClass()) { Customer customer = new Customer(mec); mec.RiseEvent1(); mec.RiseEvent2(); } Console.ReadKey(); } }
最終執行結果如下圖所示:
總結EventHandlerList的用法,在多事件型別中為每一個事件都定義了一套成員,包括事件的委託原型、事件的訂閱和取消訂閱方法,在實際應用中,可能需要定義事件專用的引數型別。這樣的設計主旨在於改動包含多事件的型別,而訂閱事件的客戶並不會察覺這樣的改動。設計本身不在於減少程式碼量,而在於有效減少多事件型別物件的大小。
2.4 如何使用事件模擬場景:貓叫->老鼠逃跑 & 主人驚醒
這是一個典型的觀察者模式的應用場景,事件的發源在於貓叫這個動作,在貓叫之後,老鼠開始逃跑,而主人則會從睡夢中驚醒。可以發現,主人和老鼠這兩個型別的動作相互之間沒有聯絡,但都是由貓叫這一事件觸發的。
設計的大致思路在於,貓類包含並維護一個貓叫的動作,主人和老鼠的物件例項需要訂閱貓叫這一事件,保證貓叫這一事件發生時主人和老鼠可以執行相應的動作。
(1)設計貓類,為其定義一個貓叫的事件CatCryEvent:
public class Cat { private string name; // 貓叫的事件 public event EventHandler<CatCryEventArgs> CatCryEvent; public Cat(string name) { this.name = name; } // 觸發貓叫事件 public void CatCry() { // 初始化事件引數 CatCryEventArgs args = new CatCryEventArgs(name); Console.WriteLine(args); // 開始觸發事件 CatCryEvent(this, args); } } public class CatCryEventArgs : EventArgs { private string catName; public CatCryEventArgs(string catName) : base() { this.catName = catName; } public override string ToString() { string message = string.Format("{0}叫了", catName); return message; } }
(2)設計老鼠類,在其構造方法中訂閱貓叫事件,並提供對應的處理方法
public class Mouse { private string name; // 在構造方法中訂閱事件 public Mouse(string name, Cat cat) { this.name = name; cat.CatCryEvent += CatCryEventHandler; } // 貓叫的處理方法 private void CatCryEventHandler(object sender, CatCryEventArgs e) { Run(); } // 逃跑方法 private void Run() { Console.WriteLine("{0}逃走了:我勒個去,趕緊跑啊!", name); } }
(3)設計主人類,在其構造犯法中訂閱貓叫事件,並提供對應的處理方法
public class Master { private string name; // 在構造方法中訂閱事件 public Master(string name, Cat cat) { this.name = name; cat.CatCryEvent += CatCryEventHandler; } // 針對貓叫的處理方法 private void CatCryEventHandler(object sender, CatCryEventArgs e) { WakeUp(); } // 具體的處理方法——驚醒 private void WakeUp() { Console.WriteLine("{0}醒了:我勒個去,叫個錘子!", name); } }
(4)最後在Main方法中進行場景的模擬:
class Program { static void Main(string[] args) { Cat cat = new Cat("假老練"); Mouse mouse1 = new Mouse("風車車", cat); Mouse mouse2 = new Mouse("米奇妙", cat); Master master = new Master("李扯火", cat); // 毛開始叫了,老鼠和主人有不同的反應 cat.CatCry(); Console.ReadKey(); } }
這裡定義了一隻貓,兩隻老鼠與一個主人,當貓的CatCry方法被執行到時,會觸發貓叫事件CatCryEvent,此時就會通知所有這一事件的訂閱者。本場景的關鍵之處就在於主人和老鼠的動作應該完全由貓叫來觸發。下面是場景模擬程式碼的執行結果:
三、反射基礎
3.1 反射的基本原理是什麼?其實現的基石又是什麼?
反射是一種動態分析程式集、模組、型別及欄位等目標物件的機制,它的實現依託於元資料。元資料,就是描述資料的資料。在CLR中,元資料就是對一個模組定義或引用的所有東西的描述系統。
3.2 .NET中提供了哪些型別實現反射?
在.NET中,為我們提供了豐富的可以用來實現反射的型別,這些型別大多數都定義在System.Reflection名稱空間之下,例如Assembly、Module等。利用這些型別,我們就可以方便地動態載入程式集、模組、型別、方法和欄位等元素。
下面我們來看一個使用示例,首先是建立一個程式集SimpleAssembly,其中有一個類為SimpleClass:
[Serializable] class SimpleClass { private String _MyString; public SimpleClass(String mystring) { _MyString = mystring; } public override string ToString() { return _MyString; } static void Main(string[] args) { Console.WriteLine("簡單程式集"); Console.Read(); } }
其次是對程式集中的模組進行分析,分別利用反射對程式集、模組和類進行分析:
public class AnalyseHelper { /// <summary> /// 分析程式集 /// </summary> public static void AnalyzeAssembly(Assembly assembly) { Console.WriteLine("程式集名字:" + assembly.FullName); Console.WriteLine("程式集位置:" + assembly.Location); Console.WriteLine("程式集是否在GAC中:" + assembly.GlobalAssemblyCache.ToString()); Console.WriteLine("包含程式集的模組名" + assembly.ManifestModule.Name); Console.WriteLine("執行程式集需要的CLR版本:" + assembly.ImageRuntimeVersion); Console.WriteLine("現在開始分析程式集中的模組"); Module[] modules = assembly.GetModules(); foreach (Module module in modules) { AnalyzeModule(module); } } /// <summary> /// 分析模組 /// </summary> public static void AnalyzeModule(Module module) { Console.WriteLine("模組名:" + module.Name); Console.WriteLine("模組的UUID:" + module.ModuleVersionId); Console.WriteLine("開始分析模組下的型別"); Type[] types = module.GetTypes(); foreach (Type type in types) { AnalyzeType(type); } } /// <summary> /// 分析型別 /// </summary> public static void AnalyzeType(Type type) { Console.WriteLine("型別名字:" + type.Name); Console.WriteLine("型別的類別是:" + type.Attributes); if (type.BaseType != null) Console.WriteLine("型別的基類是:" + type.BaseType.Name); Console.WriteLine("型別的GUID是:" + type.GUID); //設定感興趣的型別成員 BindingFlags flags = (BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance); //分析成員 FieldInfo[] fields = type.GetFields(flags); if (fields.Length > 0) { //Console.WriteLine("開始分析型別的成員"); foreach (FieldInfo field in fields) { // 分析成員 } } //分析包含的方法 MethodInfo[] methods = type.GetMethods(flags); if (methods.Length > 0) { //Console.WriteLine("開始分析型別的方法"); foreach (MethodInfo method in methods) { // 分析方法 } } //分析屬性 PropertyInfo[] properties = type.GetProperties(flags); if (properties.Length > 0) { //Console.WriteLine("開始分析型別的屬性"); foreach (PropertyInfo property in properties) { // 分析屬性 } } } }
最後編寫入口方法來嘗試分析一個具體的程式集:
[PermissionSetAttribute(SecurityAction.Demand, Name = "FullTrust")] class Program { static void Main(string[] args) { Assembly assembly = Assembly.LoadFrom(@"..\..\..\SimpleAssembly\bin\Debug\SimpleAssembly.exe"); AnalyseHelper.AnalyzeAssembly(assembly); // 建立一個程式集中的型別的物件 Console.WriteLine("利用反射建立物件"); string[] paras = { "測試一下反射效果" }; object obj = assembly.CreateInstance(assembly.GetModules()[0].GetTypes()[0].ToString(), true, BindingFlags.CreateInstance, null, paras, null, null); Console.WriteLine(obj); Console.ReadKey(); } }
上面的程式碼按照 程式集->模組->型別 三個層次的順序來動態分析一個程式集,當然還可以繼續遞迴型別內部的成員,最後通過CreateInstance方法來動態建立了一個型別,這些都是反射經常被用來完成的功能,執行結果如下圖所示:
3.3 如何使用反射實現工廠模式?
工廠模式是一種比較常用的設計模式,其基本思想在於使用不同的工廠型別來打造不同產品的部件。例如,我們在打造一間屋子時,可能需要窗戶、屋頂、門、房樑、柱子等零部件。有的屋子需要很多根柱子,而有的屋子又不需要窗戶。在這樣的需求下,就可以使用工廠模式。
(1)工廠模式的傳統實現和其弊端
下圖展示了針對屋子設計的傳統工廠模式架構圖:
上圖的設計思路是:
①使用者告訴工廠管理者需要哪個產品部件;
②工廠管理者分析使用者傳入的資訊,生成合適的實現工廠介面的型別物件;
③通過工廠生產出相應的產品,返回給使用者一個實現了該產品介面的型別物件;
通過上述思路,實現程式碼如下:
①首先是定義工廠介面,產品介面與產品型別的列舉
/// <summary> /// 屋子產品的零件 /// </summary> public enum RoomParts { Roof, Window, Pillar } /// <summary> /// 工廠介面 /// </summary> public interface IFactory { IProduct Produce(); } /// <summary> /// 產品介面 /// </summary> public interface IProduct { string GetName(); }
②其次是具體實現產品介面的產品類:窗戶、屋頂和柱子
/// <summary> /// 屋頂 /// </summary> public class Roof : IProduct { // 實現介面,返回產品名字 public string GetName() { return "屋頂"; } } /// <summary> /// 窗戶 /// </summary> public class Window : IProduct { // 實現介面,返回產品名字 public string GetName() { return "窗戶"; } } /// <summary> /// 柱子 /// </summary> public class Pillar : IProduct { // 實現介面,返回產品名字 public string GetName() { return "柱子"; } }