引子
delegate:代表,授權,翻譯為“委托”,即用對象代表方法成員或對象被授權執行方法成員。看下面一小段代碼:
int Max(int x,int y) { return x>y?x:y; } int Min(int x,int y) { return x<y?x:y; }
上面兩個函數的共同特點:具有相同的返回值和參數列表。在C++里,我們使用函數指針來指代(被授權,代表)這兩個函數。實際上,我們可以用函數指針指向任意一個具有相同返回值和參數列表的函數(靜態方法或實例的方法成員)。
//定義一個函數指針,並聲明該指針可以指向的函數的返回值為int類型,參數列表中包括兩個int類型的參數 int (*p)(int,int); //讓指針p指向Max函數 p=max; //利用指針調用Max c=(*p)(5,6);
在C#里沒有提供函數指針,取而代之的是委托(delegate);利用委托,我們可以像使用函數指針一樣在程序運行時動態指向具備相同簽名(具有相同參數類型、參數個數以及相同類型返回值)的方法。
委托的定義
之前我們在引出委托時已經簡單的介紹了委托的本質:函數指針。說的通俗一些,委托就是能夠讓方法作為變量來傳遞。我個人喜歡下面這個定義:
委托是一種類型安全的函數回調機制, 它不僅能夠調用實例方法,也能調用靜態方法,並且具備按順序執行多個方法的能力。
也就是說,委托可以在程序運行時調用不同方法函數,只要這個方法簽名和委托簽名保持一致。與函數指針不同的是,委托是類型安全的。所謂類型安全,是指在編譯時編譯器會檢測委托對象的簽名是否委托聲明一致。看下面一小段簡單的代碼感受委托的含義:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace DelegateSamples { //聲明一個委托,參數為string,無返回值 delegate void DelSamples(string msg); class Program { static void Main(string[] args) { DelSamples delSample = new Program().SpeakChinese; //調用實例方法 delSample += SpeakEnglish; //調用靜態方法 delSample("KoalaStudio"); Console.ReadKey(); } private void SpeakChinese(string msg) { Console.WriteLine("你好,我是{0}",msg); } private static void SpeakEnglish(string msg) { Console.WriteLine("Hello,I'm {0}",msg); } } }
輸出結果:
委托的聲明
簡單委托
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace DelegateSamples { //聲明一個委托,參數為string,無返回值 delegate void DelSamples(string msg); class Program { static void Main(string[] args) { //使用new關鍵字 DelSamples delSample = new DelSamples(new Program().SpeakChinese); delSample("Koala工作室"); //不使用new,自動推斷委托類型 DelSamples delSample2 = SpeakEnglish; delSample2("KoalaStudio"); //利用Lambda表達式 DelSamples delSample3 = (string msg) => SpeakEnglish(msg); delSample3("KoalaStudio"); Console.ReadKey(); } private void SpeakChinese(string msg) { Console.WriteLine("你好,我是{0}",msg); } private static void SpeakEnglish(string msg) { Console.WriteLine("Hello,I'm {0}",msg); } } }
匿名委托
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace DelegateSamples { //聲明一個委托,參數為string,無返回值 delegate void DelSamples(string msg); class Program { static void Main(string[] args) { //匿名委托 DelSamples delSample4 = delegate(string msg) { Console.WriteLine("你好,我是{0}", msg); }; delSample4("KoalaStudio"); //利用Lambda表達式實現匿名委托 DelSamples delSample5 = (string msg) => { Console.WriteLine("你好,我是{0}", msg); }; delSample5("KoalaStudio"); Console.ReadKey(); } private void SpeakChinese(string msg) { Console.WriteLine("你好,我是{0}",msg); } private static void SpeakEnglish(string msg) { Console.WriteLine("Hello,I'm {0}",msg); } } }
匿名委托的寫法更加優雅,但是需要注意兩點:1、在函數內部不能使用跳轉語句跳出函數外部;2、不能使用ref和out等關鍵字
多播委托
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace DelegateSamples { //聲明一個委托,參數為string,無返回值 delegate void DelSamples(string msg); class Program { static void Main(string[] args) { //多播委托可以帶返回值,但是只有最后一個方法的返回值會被返回。 DelSamples delSample6 = new Program().SpeakChinese; delSample6 += SpeakEnglish; delSample6("KoalaStudio"); Console.ReadKey(); } private void SpeakChinese(string msg) { Console.WriteLine("你好,我是{0}",msg); } private static void SpeakEnglish(string msg) { Console.WriteLine("Hello,I'm {0}",msg); } } }
多播委托可以連續執行函數,但是如果函數有返回值,那只有最后一個函數的返回值會被正確返回.
泛型委托
泛型委托和一般的委托用法類似,只是習慣了傳統委托的使用后在寫法上看着有些別扭。泛型委托可能在運行時確定委托的類型,提高了委托的通用性,這和泛型類的意義是一樣的。這里不再介紹泛型的概念,如果你對泛型還不熟悉,園子里有很多優秀的文章已經詳細介紹了相關內容。泛型委托包括Action、Func和Predicate三種委托。
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace DelegateSamples { class Program { static void Main(string[] args) { /* Action<T>:封裝只有一個參數(類型為T),不包括返回值的簽名函數,它包括以下幾種情況: * Action<T>、Action<T1,T2>、Action<T1,T2,T3>、Action<T1,T2,T3,T4> * 聲明: * delegate void Action(); * delegate void Action<T1>(T1 arg1); * delegate void Action<T1,T2>(T1 arg1,T2 arg2); * delegate void Action<T1,T2,T3>(T1 arg1,T2 arg2,T3 arg3); * delegate void Action<T1,T2,T3,T4>(T1 arg1,T2 arg2,T3 arg3,T4 arg4); */ Action<string> action = SpeakEnglish; action("KoalaStudio"); Action<string, string> action2 = SpeakTwoLanguage; action2("KoalaStudio","Koala工作室"); Console.ReadKey(); } private void SpeakChinese(string msg) { Console.WriteLine("你好,我是{0}",msg); } private static void SpeakEnglish(string msg) { Console.WriteLine("Hello,I'm {0}",msg); } private static void SpeakTwoLanguage(string msg1, string msg2) { Console.WriteLine("你好,我是{0}",msg1); Console.WriteLine("Hello,I'm {0}",msg2); } } }
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace DelegateSamples { class Program { static void Main(string[] args) { /* Func<T,TResult>:封裝一個具有參數(類型為T),返回TResult類型值的簽名函數,它包括以下幾種情況: * Func<T,TResult>、Func<T1,T2,TResult>、Func<T1,T2,T3,TResult>、Func<T1,T2,T3,T4,TResult> * 聲明: * ……略去 */ Func<string,string/*這是返回值類型*/> func = SpeakEnglish; func("KoalaStudio"); Func<string, string, string/*這是返回值類型*/> func2 = SpeakTwoLanguage; func2("KoalaStudio","Koala工作室"); Console.ReadKey(); } private static string SpeakEnglish(string msg) { return string.Format("Hello,I'm {0}", msg); } private static string SpeakTwoLanguage(string msg1, string msg2) { Console.WriteLine("你好,我是{0}",msg1); Console.WriteLine("Hello,I'm {0}",msg2); return string.Format("你好,我是{0};Hello,I'm {1}", msg1,msg2); } } }
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace DelegateSamples { class Program { static void Main(string[] args) { /* bool Predicate<T>:表示定義一組條件並確定指定對象是否符合這些條件的方法。 * 通常,此類委托由Array和List類的幾種方法使用,用於在集合中搜索元素。 * delegate bool Predicate<T>(T obj),如果obj符合此委托表示的方法中定義的條件,則返回true,否則返回false */ List<string> listString = new List<string>() { "a","abc","koala","xyz","take" }; //List對象的FindAll定義:public List<T> FindAll(Predicate<T> match); //match 類型:System.Predicate<T> 委托,用於定義要搜索的元素應滿足的條件。 //返回值 //類型:System.Collections.Generic.List<T> //如果找到,則為一個 List<T>,其中包含與指定謂詞所定義的條件相匹配的所有元素;否則為一個空 List<T>。 Predicate<String> match = delegate(string word) { if (word.Length > 4) { return true; } return false; }; List<string> result = listString.FindAll(match); } } }
看到這里相信大家都已經感覺出來了,委托的申明和使用和類非常相似。沒錯,委托本質上就是一個類。實際上,我們用delegate關鍵字聲明的所有委托都繼承自System.MulticastDelegate類,這個類又繼承自System.Delegate類,而System.Delegate類則繼承自System.Object。盡管如此,我們並不能直接聲明一個繼承自System.MulticastDelegate類的委托,委托必須用delegate關鍵字聲明。在我們聲明委托時,編譯器為我們完成了很多復雜的工作,有興趣的朋友可以查閱相關資料。不過,即使不清楚編譯器為我們干了什么也沒有關系,只要知道在我們調用委托時,編譯器自動為委托創建了BeginInvoke、EndInvoke和Invoke三個方法。BeginInvoke和EndInvoke方法用來實現異步委托調用,后面我們再詳細介紹。
DelSamples delSample2 = SpeakEnglish; delSample2("KoalaStudio"); //其實是調用編譯器生成的Invoke方法 delSample2.Invoke("KoalaStudio");
寫到這兒,其實已經把委托的基本概念介紹完了。接下去的內容會稍微復雜一些,剛剛接觸委托的朋友可以跳過這部分,直接閱讀委托與事件部分。
協變與逆變
MSDN的解釋:從Visual Studio2008開始.NET引入了變體支持,用於委托中匹配方法簽名和委托類型。這意味着,我們不僅可以為委托指派具有匹配簽名的方法,而且可以指派這樣的方法:它們返回與委托類型指定的派生類型相比,派生程度更大的類型(協變),或者接受相比之下,派生程度更小的類型的參數(逆變)。相當拗口,對不對?不知道它想說明什么。稍微休息一下,忘記之前那長串的描述。我們來想一個情形:面向對象的一個典型應用就是繼承和多態。假如我們定義了一個委托,它返回的類型是一個基類對象。如果有個方法,它的參數簽名符合我們定義的委托參數簽名,但是返回的是繼承該基類的子類對象,那么這個方法是否能夠使用該委托?又比如,我們定義了一個委托,它具有指定的參數簽名。如果有個方法,它的參數是委托簽名中的方法參數的基類(委托方法簽名中的參數派生自調用方法的參數),那么這個方法是否能夠使用該委托?答案是可以的!這也就是MSDN想說的:協變就是委托的類型返回值是它所指向函數(即調用的方法)的返回值的基類;逆變就是委托的類型參數是它所指向函數的參數的派生類。協變允許方法具有的派生返回類型比委托中定義的更多。 逆變允許方法具有的派生參數類型比委托類型中的更少。MSDN上提供的一個協變的例子:
class Mammals{} class Dogs : Mammals{} class Program { // 定義一個委托,返回基類. public delegate Mammals HandlerMethod(); public static Mammals MammalsHandler() { return null; } public static Dogs DogsHandler() { return null; } static void Test() { HandlerMethod handlerMammals = MammalsHandler; // 被允許. HandlerMethod handlerDogs = DogsHandler; } }
再看個逆變的例子:
// Event hander接受一個EventArgs類型的參數. private void MultiHandler(object sender, System.EventArgs e) { label1.Text = System.DateTime.Now.ToString(); } public Form1() { InitializeComponent(); // KeyDown期望接受的是KeyEventArgs對象,但是我們給的是EventArgs對象 this.button1.KeyDown += this.MultiHandler; this.button1.MouseClick += this.MultiHandler; }
協變和逆變先介紹基本的應用,其它還包括泛型委托的協變和逆變,有興趣的朋友可以參考MSDN里的例子,這里不再贅述。
異步委托
既然委托可以在運行時調用方法函數,如果調用的方法非常復雜耗時,主線程是不是會被阻塞以等待方法執行?我們看一段代碼:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; namespace DelegateSamples { public delegate void delSample (); class Program { static void Main(string[] args) { delSample _sample = DoTask; _sample(); Console.WriteLine("執行另一項工作。"); Console.ReadKey(); } public static void DoTask() { Console.WriteLine("開始執行復雜工作。"); //線程阻塞5s,模擬進行復雜的工作。 Thread.Sleep(TimeSpan.FromSeconds(5)); Console.WriteLine("工作執行完畢。"); } } }
從下面的輸出結果我們看到,委托調用的是主線程,因此復雜的方法函數會導致主線程阻塞,影響用戶體驗。那有沒有辦法可以改變呢?有!異步委托。
異步委托會從線程池中開辟一個新的線程用來調用方法,我們改動一下上面的代碼,開啟異步委托:
namespace DelegateSamples { public delegate void delSample (); class Program { static void Main(string[] args) { delSample _sample = DoTask; _sample.BeginInvoke(null, null); Console.WriteLine("執行另一項工作。"); Console.ReadKey(); } public static void DoTask() { Console.WriteLine("開始執行復雜工作。"); //線程阻塞5s,模擬進行復雜的工作。 Thread.Sleep(TimeSpan.FromSeconds(5)); Console.WriteLine("工作執行完畢。"); } } }
看一下返回結果:
好像和我們預想的不太一樣,主線程並沒有等待委托的執行,而是繼續執行下面的操作並退出。這是什么原因?還是和線程有關。默認線程池中的線程都是后台線程,主線程不會等待后台線程的執行,仿佛它根本不存在。那有辦法解決這個問題嗎?能不能讓主線程等待委托線程執行完畢在退出?我們再看一下MSDN給出的解釋:BeingInvoke方法啟動異步委托,它具有和異步執行的方法相同的參數,同時還有兩個可選參數。第一個參數是AsyncCallBack委托,該委托引用在異步調用完成時要調用的方法(即回調函數);第二個參數是一個用戶定義的對象,可以將信息傳入回調函數。BeginInvoke立即返回而不等待異步調用完成(這就解釋了上面的原因),但是會返回一個用於監控異步調用進度的IAsyncResult。EndInvoke方法檢索異步調用的結果。在調用 BeginInvoke 之后隨時可以調用該方法。如果異步調用尚未完成,則 EndInvoke 會一直阻止調用線程,直到異步調用完成。我們改一下上面的代碼:
namespace DelegateSamples { public delegate void delSample (); class Program { static void Main(string[] args) { delSample _sample = DoTask; IAsyncResult result = _sample.BeginInvoke(null, null); while (result.IsCompleted) { //可以監控異步方法執行是否完成 //do task } _sample.EndInvoke(result); Console.WriteLine("執行另一項工作。"); Console.ReadKey(); } public static void DoTask() { Console.WriteLine("開始執行復雜工作。"); //線程阻塞5s,模擬進行復雜的工作。 Thread.Sleep(TimeSpan.FromSeconds(5)); Console.WriteLine("工作執行完畢。"); } } }
從輸出結果看,委托線程確實阻塞了主線程,但是這依然會影響用戶體驗。我們希望的流程是:主線程調用委托線程后繼續執行后面的邏輯業務,等邏輯業務完成后等待委托線程的返回。我們利用WaitHandle等待異步調用來解決這個問題,異步調用完成時會發出WaitHandle信號,我們可以通過調用WaitOne方法等待該信號。
namespace DelegateSamples { public delegate void delSample (); class Program { static void Main(string[] args) { delSample _sample = DoTask; IAsyncResult result = _sample.BeginInvoke(null, null); //等待1s,執行主線程 result.AsyncWaitHandle.WaitOne(1,false);
//_sample(); //_sample.EndInvoke(result); Console.WriteLine("執行另一項工作。"); _sample.EndInvoke(result); Console.ReadKey(); } public static void DoTask() { Console.WriteLine("開始執行復雜工作。"); //線程阻塞5s,模擬進行復雜的工作。 Thread.Sleep(TimeSpan.FromSeconds(5)); Console.WriteLine("工作執行完畢。"); } } }
再看一種回調模式
namespace DelegateSamples { public delegate void delSample (); class Program { static void Main(string[] args) { delSample _sample = DoTask; _sample.BeginInvoke( delegate(IAsyncResult ar) { _sample.EndInvoke(ar); },null ); Console.WriteLine("執行另一項工作。"); Console.ReadKey(); } public static void DoTask() { Console.WriteLine("開始執行復雜工作。"); //線程阻塞5s,模擬進行復雜的工作。 Thread.Sleep(TimeSpan.FromSeconds(5)); Console.WriteLine("工作執行完畢。"); } } }
回調模式是在BeginInvoke后立刻調用回調函數,這個回調函數是在 ThreadPool線程上進行的,因此主線程將繼續執行。ThreadPool線程是后台線程,這些線程不會在主線程結束后保持應用程序的運行,因此主線程必須休眠足夠長的時間以便回調完成。我們也可以在主線程完成操作后調用IsCompleted屬性判斷委托函數是否完成。
委托與事件
既然說了委托,自然離不開事件。在這里我不打算詳細闡述事件機制及其相關的內容,后面我會再整理一篇有關事件的文章。我打算從委托出發,看看事件是怎么和委托扯上關系的。先看一段代碼:
namespace DelegateSamples { public class Program { static void Main(string[] args) { Publish publish = new Publish(); Subscribe subscribe = new Subscribe(); publish.delEventHandler += subscribe.OnLeaveMessage; publish.OnFire("我觸發了委托!"); //調用委托 publish.delEventHandler("我觸發了委托!"); Console.ReadKey(); } } //定義委托 public delegate void FireEventHandler(string msg); public class Publish { public FireEventHandler delEventHandler; public void OnFire(string msg) { if (delEventHandler != null) { delEventHandler(msg); } } } public class Subscribe { public Subscribe(){} public void OnLeaveMessage(string msg) { Console.WriteLine(msg); } } }
這里我們發現,如果要使用委托變量,那么在類的內部委托變量必須是public,這就顯得不安全。我只希望外部通過Publish.OnFire()來執行委托,但是外部仍然可以直接調用委托。於是,我們換一個寫法,再看代碼:
namespace DelegateSamples { public class Program { static void Main(string[] args) { Publish publish = new Publish(); Subscribe subscribe = new Subscribe(); publish.delEventHandler += subscribe.OnLeaveMessage; publish.OnFire("我觸發了委托!"); //調用委托 publish.delEventHandler("我觸發了委托!"); Console.ReadKey(); } } //定義委托 public delegate void FireEventHandler(string msg); public class Publish { //public FireEventHandler delEventHandler; //不使用委托關鍵字delegate,采用event關鍵字 public event FireEventHandler delEventHandler; public void OnFire(string msg) { if (delEventHandler != null) { delEventHandler(msg); } } } public class Subscribe { public Subscribe(){} public void OnLeaveMessage(string msg) { Console.WriteLine(msg); } } }
publish.delEventHandler("我觸發了委托!"),編譯無法通過,這意味着外部不能直接調用委托變量,盡管他的屬性還是public。因為加了event關鍵字,本質上是生成了私有的委托變量,減少了外部對類內部變量的修改權利。說到這里,我們再來看一下事件和委托的關系。在CLR綜合那個事件模型是建立在委托機制上的,這就是他們之間的聯系。委托是對方法的抽象,它將方法的調用與實現分離,方法的調用者(即委托的執行者)不知道方法內部的實現,而方法的實現者也不知道方法什么時候會被調用。正是因為委托具有這樣的特性,才使得它理所當然的用來實現事件機制。因為事件被觸發后執行什么操作,是由觸發者決定的,而事件擁有者只知道什么情況下會觸發事件,但不知道事件的具體實現。
最后,我們看一下事件的定義。事件用event關鍵字定義,其類型是一個委托類型,這體現了事件是通過委托來實現的。上面的代碼已經展示了最基本的事件定義方式,即:自定義一個委托,然后再申明一個event事件。在.NET3.5開始,由於支持泛型,定義事件時可以不需要再自定義委托,統一采用System.EventHandler<TEventArgs>委托,如何改造上面代碼,請大家自行修改。