引言: 感覺好久沒有更新博客了的,真是對不住大家了。在這個專題中將介紹匿名方法,匿名方法看名字也能明白,當然就是沒有名字的方法了(現實生活中也有很多這樣的匿名過程,如匿名投票,匿名舉報等等,相信微軟在命名方面肯定是根據了生活中例子的),然而匿名方法的理解卻不是僅僅是這一句話(這句話指的是沒有名字的方法),它還有很多內容,下面就具體介紹下匿名方法有哪些內容 一、匿名方法 之前一直認為匿名方法是在C# 3.0中提出的,之前之所以這麼認為主要是因為知道C# 3.0中提出了匿名類型,所以看到匿名方法就很理所當然的認為也是在C# 3.0中提出來,然而經過系統的學習C#特性後才發現匿名方法在C# 2.0 的時候就已經提出來了,從特性的提出發展中可以看出,微軟的團隊是非常有計劃的,後面的特性其實在之前特性的提出就已經計劃好,並且後面的特性都是之前特性演變而來,之所以有新特性的提出,主要是為了方便大家編寫程序,減輕程序員的工作,讓編譯器去執行更加復雜的操作,使程序員可以把精力放在實現自己系統的業務邏輯方法(這也是微軟的主要思想,也是大部分軟件所強調的良好的用戶體驗),然而匿名方法也正是建立在C#1.0中委托的基礎上的(同時C# 2.0中對委托有所增強,提出了泛型委托,以及委托參數的協變和逆變,具體的可以參考本系列的前面專題),下面就具體介紹下為什麼說匿名方法是如何建立在委托基礎之上的(委托是方法的包裝,匿名方法也是方法,只是匿名方法是沒有名字的方法而已,所以委托也可以包裝匿名方法)。 首先,先介紹下匿名方法的概念,匿名方法——沒有名字的方法(方法也就是數學中的函數的概念),匿名方法只是在我們編寫的源代碼中沒有指定名字而已,其實編譯器會幫匿名方法生成一個名字,然而就是因為在源代碼中沒有名字,所以匿名方法只能在定義的時候才能調用,在其他地方不能被調用(匿名方法把方法的定義和方法的實現內嵌在一起),下面通過一個例子來看看匿名方法的使用和如何與委托關聯起來的: namespace 匿名方法Demo { class Program { // 定義投票委托 delegate void VoteDelegate(string name); static void Main(string[] args) { // 實例化委托對象 VoteDelegate votedelegate = new VoteDelegate(new Friend().Vote); // 使用匿名方法的代碼 // 匿名方法內聯了一個委托實例(可以對照上面的委托實例化的代碼來理解) // 使用匿名方法後,我們就不需要定義一個Friend類以及單獨定義一個投票方法 // 這樣就可以減少代碼量,代碼少了,閱讀起來就容易多了,以至於不會讓過多的回調方法的定義而弄糊塗了 //VoteDelegate votedelegate = delegate(string nickname) //{ // Console.WriteLine("昵稱為:{0} 來幫Learning Hard投票了", nickname); //}; // 通過調用托來回調Vote()方法 votedelegate("SomeBody"); Console.Read(); } } public class Friend { // 朋友的投票方法 public void Vote(string nickname) { Console.WriteLine("昵稱為:{0} 來幫Learning Hard投票了", nickname); } } } 因為前段時間參加了51博客大賽,在投票階段也拉了好多朋友來幫忙投票的,所以為了感謝他們,所以上面就以投票作為例子來引出匿名方法,注釋的部分中已經解釋了匿名方法的好處的,可以幫助我們減少書寫代碼量,便於閱讀,然而上面地方可以使用匿名方法來代替委托呢?是不是所有使用委托的地方我們都需要用匿名方法去代替的呢?事實不是這樣的,因為匿名方法是沒有名字的方法,所以在其他地方就不能被調用,所以不具有復用作用,並且匿名方法自動形成"閉包"(如果對於閉包不理解的朋友可以參考這兩個鏈接:http://baike.baidu.com/view/648413.htm 和http://zh.wikipedia.org/wiki/閉包_(計算機科學) ,我理解的閉包大概是當一個函數中(外部函數)調用了另個一個函數(稱內部函數)時,當內部函數使用了外部函數中的變量時,這樣就可能會形成閉包。具體的概念可以參考上面的兩個鏈接,關於閉包在後面部分也會給出相關的例子來幫助大家理解,由於匿名函數會形成閉包,這就會延長變量的生命周期)。所以如果委托包裝的方法相對簡單(就像上面代碼中只是單獨一行輸出語句),並且這個方法在其他地方使用的頻率很低時,這時候就可以考慮用匿名方法來代替委托。 二、使用匿名方法來忽略委托參數 第一部分主要介紹了匿名方法的概念,使用以及介紹了我所理解的為什麼會有匿名方法的提出(為了方便我們實例化委托實例,通過匿名方法可以內聯委托實例,這樣就避免額外定義一個實例方法,減少代碼量,利於閱讀),在這一部分中將介紹匿名方法的另外一個好處——忽略委托參數。下面通過一個示例代碼來來幫助大家理解,代碼中會有詳細的注釋,所以這裡就不多說了,直接看代碼了: namespace 忽略委托參數Demo { class Program { static void Main(string[] args) { // Timer類在應用程序中生成定期事件 System.Timers.Timer timer = new System.Timers.Timer(); // 該值指示是否引發Elapsed事件 timer.Enabled = true; // 設置引發Elapsed事件的間隔 timer.Interval = 1000; // Elapsed事件是達到間隔時發生,前面設置了時間間隔為1秒, // 所以每一秒就會觸發Elapsed事件,從而回調timer_Elapsed方法,輸出當前的時間 // timer.Elapsed += new System.Timers.ElapsedEventHandler(timer_Elapsed); // 此時timer_Elapsed方法中的參數根本就不需要,所以我們可以使用匿名方法來省略委托參數 // 省略了參數後我們的代碼就更加簡潔了,看的多舒服啊 // 在開發WinForm程序中我們經常會用不到委托的參數,此時就可以使用匿名方法來省略參數 timer.Elapsed += delegate { Console.WriteLine(DateTime.Now); }; Console.Read(); } public static void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) { Console.WriteLine(DateTime.Now); } } } 運行結果為: 上面代碼使用了匿名方法來省略委托參數,然而對於編譯器而言,它還是會調用委托的構造函數來實例化委托,所以如果匿名方法能轉換為多個委托類型時,此時如果省略了委托參數,編譯器就不知道把匿名方法轉化為哪個具體的委托類型,所以此時就會出現編譯時錯誤,此時就必須人為的指定參數來告訴編譯器如何實例化委托,下面就以創建線程為例子來幫助大家理解匿名方法省略委托參數所帶來的問題(因為線程的創建涉及了兩個委托類型: public delegate void ThreadStart() 和public delegaye void ParameterizedThreadStart(objec obj)): class Program { static void Main(string[] args) { new Thread(delegate() { Console.WriteLine("線程一"); }); new Thread(delegate(object o) { Console.WriteLine("線程二"); }); new Thread(delegate { Console.WriteLine("線程三"); }); Console.Read(); } } 此時第三個創建線程的代碼會出現下面的編譯錯誤: 三、在匿名方法中捕捉變量 前面介紹中提到使用匿名方法時會形成閉包,閉包指的就是在匿名方法中捕捉了變量,為了更好的理解閉包的概念,首先需要理解兩個概念 ——外部變量和被捕捉的外部變量,下面通過一個例子來解釋這個兩個概念: class Program { // 定義閉包委托 delegate void ClosureDelegate(); static void Main(string[] args) { closureMethod(); Console.Read(); } // 閉包方法 private static void closureMethod() { // outVariable和capturedVariable對於匿名方法而言都是外部變量 // 然而outVariable是未捕獲的外部變量,子所以是未捕獲,是因為匿名方法中未引用該變量 string outVariable = "外部變量"; // 而capturedVariable是被匿名方法捕獲的外部變量 string capturedVariable = "捕獲變量"; ClosureDelegate closuredelegate = delegate { // localvariable是匿名方法中局部變量 string localvariable = "匿名方法局部變量"; Console.WriteLine(capturedVariable+" "+localvariable); }; // 調用委托 closuredelegate(); } } 一個變量被捕捉後,被匿名方法捕捉到的是真的變量,而不是創建委托實例時該變量的值,並且被匿名方法中捕捉到的變量會延長生命周期(意思是說對於一個被捕捉的變量,只要還有任何委托實例引用它,它就一直存在,而不會當委托實例調用結束後就被垃圾回收),下面通過一個具體的例子看看匿名方法是如何延長變量的生命周期的: class Program { // 定義閉包委托 delegate void ClosureDelegate(); static void Main(string[] args) { ClosureDelegate test = CreateDelegateInstance(); test(); Console.Read(); } // 閉包延長變量的生命周期 private static ClosureDelegate CreateDelegateInstance() { int count = 1; ClosureDelegate closuredelegate = delegate { Console.WriteLine(count); count++; }; // 調用委托 closuredelegate(); return closuredelegate; } } 運行結果為: 第一行中的1是CreateDelegateInstance內部調用委托實例輸出的結果,首先大家肯定認為count是在棧上分配的(因為count是值類型),當CreateDelegateInstance方法調用完後,count的值也會被銷毀,當執行 test()這行代碼時,此時會回調匿名方法來輸出count的值,因為count被銷毀,按理應該會出現異常才對的,然而結果卻為2,然而結果並沒有錯,根據結果去倒推的話,可以得出,第二次調用委托實例也還是在使用原來的那個count,然而之所以我們認為會有異常拋出,主要原因是因為我們認為count是分配在棧上的,然而事實並不是這樣的,count變量並不是分配在棧上的,事實上,編譯器會創建一個額外的類來容納變量(此時count變量時分配在堆上的),CreateDelegateInstance方法有該類的一個實例的引用,所以此時匿名方法捕捉到的變量count是它的一個引用,而不是真真的值,同時匿名方法也延長了變量count的生命周期,使它感覺不再像是一個局部變量,反而像是一個"全局變量"了(因為第二次中調用的委托實例使用的是同一個count)。 匿名方法捕捉到的變量,編譯器會額外創建一個類來容納該變量,對於這點,大家可以通過IL反匯編程序進行查看,下面是上面程序中使用反匯編程序得到的截圖: 從上面的截圖中可以看出,在源代碼中根本沒有<>c_DisplayClass1類的定義的,然而這個類真是編譯器為我們創建來容納捕獲變量count的,並且該類中容納了CreateDelegateInstance方法,從上圖的左半部分中間語言代碼可以看出,源代碼中定義的CreateDelegateInstance方法具有該<>c_DisplayClass1的一個引用,在源代碼中使用到的count變量編譯器認為是<>c_DisplayClass1中的一個字段。 四、小結 這個專題中主要介紹了匿名方法的使用以及匿名方法通過捕獲變量來延長變量的生命周期,希望通過本專題的介紹大家可以對匿名方法可以有個全面的認識,並且匿名方法也是Lambda表達式的基礎,Lambda表達式只是C# 3.0中提出更簡潔的方式來實現匿名方法的。