委托
委托類似於 C++ 中的函數指針(一個指向內存位置的指針)。委托是 C# 中類型安全的,可以訂閱一個或多個具有相同簽名方法的函數指針。簡單理解,委托是一種可以把函數當做參數傳遞的類型。很多情況下,某個函數需要動態地去調用某一類函數,這時候我們就在參數列表放一個委托當做函數的占位符。在某些場景下,使用委托來調用方法能達到減少代碼量,實現某種功能的用途。
自定義委托
聲明和執行一個自定義委托,大致可以通過如下步驟完成:
- 利用關鍵字 delegate 聲明一個委托類型,它必須具有和你想要傳遞的方法具有相同的參數和返回值類型;
- 創建委托對象,並且將你想要傳遞的方法作為參數傳遞給委托對象;
- 通過上面創建的委托對象來實現該委托綁定方法的調用。
下面一段代碼,完成了一次應用委托的演示:
//step01:使用delegate關鍵字聲明委托 public delegate int CalcSumDelegate(int a, int b); class Program { static void Main(string[] args) { //step03:實例化這個委托,並引用方法 CalcSumDelegate del = new CalcSumDelegate(CalcSum); //step04:調用委托 int result = del(5, 5); Console.WriteLine("5+5=" + result); } //step02:聲明一個方法和委托類型對應 public static int CalcSum(int a, int b) { return a + b; } }
通過上面 4 個步驟,完成了委托從聲明到調用的過程。接着,使用 ILSpy 反編譯上面的代碼生成的程序集,截圖如下:
- 自定義委托繼承關系是:System.MulticastDelegate —> System.Delegate —>System.Object。
- 委托類型自動生成3個方法:BeginInvoke、EndInvoke、Invoke。查資料得知,委托正是通過這3個方法在內部實現調用的。Invoke 方法,允許委托同步調用。上面調用委托的代碼 del(5, 5) 執行時,編譯器會自動調用 del.Invoke(5,5);BeginInvoke 方法,允許委托的異步調用。假如上述委托以異步的方式執行,則要顯示調用 dal.BeginInvoke(5,5)。
注意:BeginInvoke 和 EndInvoke 是.Net中使用異步方式調用同步方法的兩個重要方法,具體用法詳見微軟 官方文檔。
多播委托
一個委托可以引用多個方法,包含多個方法的委托就叫多播委托。下面通過一個示例來了解什么是多播委托:
//step01:聲明委托類型 public delegate void PrintDelegate(); public class Program { public static void Main(string[] args) { //step03:實例化委托,並綁定第1個方法 PrintDelegate del = Func1; //綁定第2個方法 del += Func2; //綁定第3個方法 del += Func3; //step04:調用委托 del(); //控制台輸出結果: //調用第1個方法! //調用第2個方法! //調用第3個方法! } //step02:聲明和委托對應簽名的3個方法 public static void Func1() { Console.WriteLine("調用第1個方法!"); } public static void Func2() { Console.WriteLine("調用第2個方法!"); } public static void Func3() { Console.WriteLine("調用第3個方法!"); } }
可以看出,多播委托的聲明過程是和自定義委托一樣的,可以理解為,多播委托就是自定義委托在實例化時通過 “+=” 符號多綁定了兩個方法。
Q:為什么能給委托綁定多個方法呢?
自定義委托的基類就是多播委托MulticastDelegate ,這就要看看微軟是如何對 System.MulticastDelegate 定義的:
MulticastDelegate 擁有一個帶有鏈接的委托列表,該列表稱為調用列表,它包含一個或多個元素。在調用多路廣播委托時,將按照調用列表中的委托出現的順序來同步調用這些委托。如果在該列表的執行過程中發生錯誤,則會引發異常。(--摘自MSDN)
Q:為什么使用“+=”號就能實現綁定呢?
先來看上述程序集反編譯后的調用委托的代碼:

“+=”的本質是調用了Delegate.Combine方法,該方法將兩個委托連接在一起,並返回合並后的委托對象。
Q:多播委托能引用多個具有返回值的方法嗎?
答案是,當然能。委托的方法可以是無返回值的,也可以是有返回值的。不過,對於有返回值的方法需要我們從委托列表上手動調用。否則,就只能得到委托調用的最后一個方法的結果。下面通過兩段代碼驗證下:

public delegate string GetStrDelegate(); public class Program { public static void Main(string[] args) { GetStrDelegate del = Func1; del += Func2; del += Func3; string result = del(); Console.WriteLine(result); //控制台輸出結果: //You called me from Func3 } public static string Func1() { return "You called me from Func1!"; } public static string Func2() { return "You called me from Func2!"; } public static string Func3() { return "You called me from Func3!"; } }
正確做法:利用 GetInvocationList 獲得委托列表上所有方法,循環依次執行委托,並處理委托返回值。

public delegate string GetStrDelegate(); public class Program { public static void Main(string[] args) { GetStrDelegate del = Func1; del += Func2; del += Func3; //獲取委托鏈上所有方法 Delegate[] delList = del.GetInvocationList(); //遍歷,分別處理每個方法的返回值 foreach (GetStrDelegate item in delList) { //執行當前委托 string result = item(); Console.WriteLine(result); //控制台輸出結果: //You called me from Func1 //You called me from Func2 //You called me from Func3 } Console.ReadKey(); } public static string Func1() { return "You called me from Func1"; } public static string Func2() { return "You called me from Func2"; } public static string Func3() { return "You called me from Func3"; } }
匿名方法
匿名方法是 C#2.0 版本引入的一個新特性,用來簡化委托的聲明。假如委托引用的方法只使用一次,那么就沒有必要聲明這個方法,這時用匿名方法表示即可。
//step01:定義委托類型 public delegate string ProStrDelegate(string str); public class Program { public static void Main(string[] args) { //step02:將匿名方法指定給委托對象 ProStrDelegate del = delegate(string str) { return str.ToUpper(); }; string result = del("KaSlFkaDhkjHe"); Console.WriteLine(result); Console.ReadKey(); //輸出:KASLFKAFHKJHE } }
匿名方法只是 C# 提供的一個語法糖,方便開發人員使用。在性能上與命名方法幾乎無異。
匿名方法通常在下面情況下使用:
- 委托需要指定一個臨時方法,該方法使用次數極少;
- 這個方法的代碼很短,甚至可能比方法聲明都短的情況下使用。
Lambda表達式
Lambda 表達式是 C#3.0 版本引入的一個新特性,它提供了完成和匿名方法相同目標的更加簡潔的格式。下面示例用 Lambda 表達式簡化上述匿名方法的例子:
public delegate string ProStrDelegate(string str); public class Program { public static void Main(string[] args) { //匿名委托 //ProStrDelegate del = delegate(string str) { return str.ToUpper(); }; //簡化1 //ProStrDelegate del1 = (string str) =>{ return str.ToUpper(); }; //簡化2 //ProStrDelegate del2 = (str) =>{ return str.ToUpper(); }; //簡化3 ProStrDelegate del3 = str => str.ToUpper(); string result = del3("KaSlFkaDhkjHe"); Console.WriteLine(result); Console.ReadKey(); //輸出:KASLFKAFHKJHE } }
- 簡化1:去掉 delegate 關鍵字,用"=>"符號表示參數列表和方法體之間的關系;
- 簡化2:去掉方法的參數類型;假如只有一個參數,參數列表小括號()也可省略;
- 簡化3:如果方法體中的代碼塊只有一行,可以去掉 return,去掉方法體的大括號{}。
內置委托
上述幾種委托的使用,都沒能離開定義委托類型這一步驟。微軟干脆直接把定義委托這一步驟封裝好,形成三個泛型類:Action<T>、Func<T> 和 Predicate<T>,這樣就省去了定義的步驟,推薦使用。
public class Program { public static void Main(string[] args) { //Action Action<string> action = delegate(string str) { Console.WriteLine("你好!" + str); }; action("GG"); //Func Func<int, int, int> func = delegate(int x, int y) { return x + y; }; Console.WriteLine("計算結果:" + func(5, 6)); //Predicate Predicate<bool> per = delegate(bool isTrue) { return isTrue == true; }; Console.WriteLine(per(true)); } }
它們的區別如下:
- Action<T> 委托:允許封裝的方法有多個參數,不能有返回值;
- Func<T> 委托:允許封裝的方法有多個參數,必須有返回值;
- Predicate<T> 委托:允許封裝的方法有一個參數,返回值必須為 bool 類型。
事件
委托是一種類型,事件依賴於委托,故事件可以理解為是委托的一種特殊實例。它和普通的委托實例有什么區別呢?委托可以在任意位置定義和調用,但是事件只能定義在類的內部,只允許在當前類中調用。所以說,事件是一種類型安全的委托。
定義事件
通過一個簡單的場景來演示下事件的使用:
/// <summary> /// 音樂播放器 /// </summary> public class MusicPlayer { //step01:定義 音樂播放結束 事件 public event EventHandler<EventArgs> PlayOverEvent; public string Name { get; set; } public MusicPlayer(string name) { this.Name = name; } //step02:定義一個觸發事件的方法 public void PlaySong() { //模擬播放 Console.WriteLine("正在播放歌曲:" + this.Name); for (int i = 0; i < 20; i++) { Console.Write("."); Thread.Sleep(100); } //播放結束,則觸發PlayOverEvent事件 if (PlayOverEvent != null) { PlayOverEvent(this, null); } } } public class Program { static void Main(string[] args) { //創建音樂播放器對象 MusicPlayer player = new MusicPlayer("自由飛翔"); //step03:注冊事件 player.PlayOverEvent += player_PlayOverEvent; //播放歌曲,結束后觸發事件 player.PlaySong(); Console.ReadKey(); } static void player_PlayOverEvent(object sender,EventArgs e) { MusicPlayer player = sender as MusicPlayer; Console.WriteLine("\r\n{0}播完了!", player.Name); } }
程序運行結果:
總結上面事件使用的幾個步驟:
- 用event關鍵字定義事件,事件必須要依賴一個委托類型;
- 在類內部定義觸發事件的方法;
- 在類外部注冊事件並引發事件。
public event EventHandler<EventArgs> PlayOverEvent
這句代碼在 MusicPlayer 類定義了一個事件成員 PlayOverEvent,我們說事件依賴於委托、是委托的特殊實例,所以 EventHandler<EventArgs> 肯定是一個委托類型,下面我們來驗證一下。
EventHandler 是微軟封裝好的事件委托,該委托沒有返回值類型,兩個參數:sender 事件源一般指的是事件所在類的實例;TEventArgs 事件參數,如果有需要創建,要顯示繼承 System.EventArgs。
事件的本質
MusicPlayer player = new MusicPlayer("自由飛翔"); //注冊事件 player.PlayOverEvent += player_PlayOverEvent; player.PlaySong();
從上面代碼我們觀察到,事件要通過"+="符號來注冊。我們猜想,事件是不是像多播委托一樣通過 Delegate.Combine 方法可以綁定多個方法?還是通過反編譯工具查看下。
我們看到 PlayOverEvent 事件內部生成了兩個方法:add_ PlayOverEvent 和 remove_ PlayOverEvent。add 方法內部調用 Delegate.Combine 把事件處理方法綁定到委托列表;remove 方法內部調用 Delegate.Remove 從委托列表上移除指定方法。其實,事件本質上就是一個多播委托。
參考文章
[1] Edison Chou,http://www.cnblogs.com/edisonchou/p/4827578.html
[2] jackson0714,http://www.cnblogs.com/jackson0714/p/5111347.html
[3] Sam Xiao, http://www.cnblogs.com/xcj26/p/3536082.html