引言:
C# 3中所有特性的提出都是更好地為Linq服務的, 充分理解這些基礎特性后。對於更深層次地去理解Linq的架構方面會更加簡單,從而就可以自己去實現一個簡單的ORM框架的,對於Linq的學習在下一個專題中將會簡單和大家介紹下,這個專題還是先來介紹服務於Linq的基礎特性——擴展方法
一、擴展方法的介紹
我一般理解一個知識點喜歡拆分去理解,所以對於擴展方法的理解可以拆分為——首先它肯定是一個方法,然而方法又是對於一個類型而言的,所以擴展方法可以理解為現有的類型(現有類型可以為自定義的類型和.Net 類庫中的類型)擴展(添加)應該附加到該類型中的方法。
在沒有擴展方法之前,如果我們想為一個已有類型自定義自己邏輯的方法時,我們必須自定義一個新的類型來繼承已有類型的方式來添加方法,使用這種繼承方式來添加方法時,我們必須自定義一個新的派生類型,如果基類有抽象方法還需要重新去實現抽象方法,這樣為了擴展一個方法卻會導致因繼承而帶來的其他的開銷(指的是又要去自定義一個派生類,還要覆蓋基類的抽象方法等),所以使用繼承來為現有類型擴展方法時就有點大才小用的感覺了,並且當我們需要為值類型和密封類(不能被繼承的類)這些不能被繼承的類型擴展方法時,此時繼承就不能被我們所用了, 所以在C#3 中提出了用擴展方法來實現為現有類型添加方法。使用擴展方法來實現擴展可以解決使用繼承中所帶來的所有的弊端,下面通過一個例子來演示下擴展方法的使用:
class Program { /// <summary> /// 擴展方法演示 /// </summary> /// <param name="args"></param> static void Main(string[] args) { #region 演示擴展方法的使用 // 調用擴展方法 WebRequest request = WebRequest.Create("http://www.cnblogs.com"); using (WebResponse response = request.GetResponse()) { using(Stream responsestream =response.GetResponseStream()) { using (FileStream output = File.Create("response.htm")) { // 調用擴展方法 responsestream.CopyToNewStream(output); Console.Read(); } } } #endregion } } /// <summary> /// 擴展方法必須在非泛型靜態類中定義 /// </summary> public static class StreamExten { // 定義擴展方法 // 該擴展方法實現從一個流中內容復制到另一個流中 public static void CopyToNewStream(this Stream inputstream, Stream outputstream) { byte[] buffer = new byte[8192]; int read; while ((read = inputstream.Read(buffer, 0, buffer.Length)) > 0) { outputstream.Write(buffer, 0, read); } } }
上面程序中為Stream類型擴展了一個CopyToNewStream()的方法,然而從上面擴展方法的定義中大家可以知道擴展方法定義的一些規則,然而並不是所有方法都可以作為擴展方法來使用的, 此時朋友們就會問,我如何去分辨代碼中定義的是擴展方法還是普通的方法呢? 對於這個疑問,擴展方法的定義是要符合一些規則的,當看到定義的方法是符合這個規則,則就可以確定定義方法是擴展方法還是普通方法了。擴展方法必須具備下面的規則:
- 它必須在一個非嵌套、非泛型的靜態類中
- 它至少要有一個參數
- 第一個參數必須加上this關鍵字作為前綴(第一個參數類型也稱為擴展類型,即指方法對這個類型進行擴展)
- 第一個參數不能用其他任何修飾符(如不能使用ref out等修飾符)
- 第一個參數的類型不能是指針類型
對於上面的規則大家可以在代碼中試驗下就會很容易明白,這些規則是一些硬性的規定,如果違反了這些規則,編譯器可能會報錯或者說編譯器將不會認為定義的方法為擴展方法,下面簡單演示下擴展方法必須在非嵌套類型的靜態類中這個規則(其他規則同樣大家可以在代碼中進行測試),當我們把上面代碼中StreamExten 類定義為Program嵌套類型時,編譯器此時就會出現"擴展方法必須在頂級靜態類中定義;STreamExten是嵌套類"的編譯時錯誤,演示代碼如下:

class Program { /// <summary> /// 擴展方法必須在非泛型靜態類中定義 /// </summary> public static class StreamExten { // 定義擴展方法 // 該擴展方法實現從一個流中內容復制到另一個流中 public static void CopyToNewStream(this Stream inputstream, Stream outputstream) { byte[] buffer = new byte[8192]; int read; while ((read = inputstream.Read(buffer, 0, buffer.Length)) > 0) { outputstream.Write(buffer, 0, read); } } } /// <summary> /// 擴展方法演示 /// </summary> /// <param name="args"></param> static void Main(string[] args) { #region 演示擴展方法的使用 // 調用擴展方法 WebRequest request = WebRequest.Create("http://www.cnblogs.com"); using (WebResponse response = request.GetResponse()) { using(Stream responsestream =response.GetResponseStream()) { using (FileStream output = File.Create("response.htm")) { // 調用擴展方法 responsestream.CopyToNewStream(output); Console.Read(); } } } #endregion } }
下面是出現編譯時錯誤截圖:
二、擴展方法是如何被發現的?
從上面部分的介紹,朋友們應該知道了如何定義和使用一個擴展方法,並且從我們定義的規則中可以幫助我們開發人員更好地去識別擴展方法,知道程序中調用的是一個實例方法還是一個擴展方法,然而相信大家此時會有這樣一個疑問——編譯器是如何知道我調用的是一個擴展方法而不是一個該類中的一個實例方法呢?對於這個問題,將在這部分和大家分析下。
首先討論下程序員是如何去識別調用的是一個擴展方法而不是一個實例方法的,當我們看到調用方法的代碼時,首先我們會去找該方法是否是該類(如上面程序中的Stream類)的一個實例方法,進入Stream類(按F12進去查看)的定義中卻發現該類中沒有一個名為CopyToNewStream的方法,此時我們就會查看程序中是否定義了這樣的擴展方法,當找到一個為名CopyToNewStream這樣的方法時,然后再根據定義的規則來判斷找到的方法是否是為Stream類擴展的方法,這樣的一個過程就是我們程序員去發現一個擴展方法的過程,然而對於編譯器而言,它也是這么去發現擴展方法的(從而可以看出C#編譯器還是非常智能的,完全按照人的思路去思考問題,因為它也是人實現出來的,就當然是盡可能地去以人的思考方式去實現的了),下面就介紹下編譯器是如何去發現擴展方法的,這樣也可以與程序員們的思路進行對比下。
當編譯器看到變量調用的是一個方法時,它首先會去該對象中實例方法中去查看,一旦沒有找到與調用方法同名的實例方法時,編譯器就會去查找一個合適的擴展方法,它會檢查導入的所有命名空間和當前的命名空間中的所有擴展方法,並匹配變量類型到擴展類型存在一個隱式轉換的擴展方法。然而對於這個發現過程,可能有些人會問:編譯器如何知道某個方法是擴展方法而不是實例方法呢? 編譯器是根據System.Runtime.CompilerServices.ExtensionAttribute屬性來綁定方法是是否為擴展方法的, 當我們定義的方法是擴展方法時,該屬性會自動應用到方法上,編譯器還會將該特性應用到包含擴展方法的程序集上,對於這個兩點並不是我的推斷,下面給出反編譯截圖來證明下:
從上面編譯器發現擴展方法的過程可以得到方法調用的優先級的結論:現有的實例方法——>當前命名空間下的擴展方法——>導入命名空間的擴展方法。下面通過一個例子來演示編譯器的發現過程:
using System; namespace 擴展方法如何被發現Demo { // 要使用不同命名空間的擴展方法首先要添加該命名空間的引用 using CustomNamesapce; class Program { static void Main(string[] args) { Person p = new Person { Name = "Learning hard" }; // 當類型中包含了實例方法時,VS中的智能提示就只會列出實例方法,而不會列出擴展方法 // 當把實例方法注釋掉之后,VS的智能提示中才會列出擴展方法,此時編譯器在Person類型中找不到實例方法 // 所以首先從當前命名空間下查找是否有該名字的擴展方法,如果找到不會去其他命名空間中查找了 // 如果在當前命名空間中沒有找到,則會到導入的命名空間中再進行查找 p.Print(); p.Print("Hello"); Console.Read(); } } // 自定義類型 public class Person { public string Name { get; set; } // 當類型中的實例方法 ////public void Print() ////{ //// Console.WriteLine("調用實例方法輸出,姓名為: {0}", Name); ////} } // 當前命名空間下的擴展方法定義 public static class Extensionclass { /// <summary> /// 擴展方法定義 /// </summary> /// <param name="per"></param> public static void Print(this Person per) { Console.WriteLine("調用的是同一命名空間下的擴展方法輸出,姓名為: {0}", per.Name); } } } namespace CustomNamesapce { using 擴展方法如何被發現Demo; public static class CustomExtensionClass { /// <summary> /// 擴展方法定義 /// </summary> /// <param name="per"></param> public static void Print(this Person per) { Console.WriteLine("調用的是不同命名空間下擴展方法輸出,姓名為: {0}", per.Name); } /// <summary> /// 擴展方法定義 /// </summary> /// <param name="per"></param> public static void Print(this Person per,string s) { Console.WriteLine("調用的是不同命名空間下擴展方法輸出,姓名為: {0}, 附加字符串為{1}", per.Name, s); } } }
運行結果:
當沒有注釋掉Person類中的實例方法Print時,此時在p后面鍵入.運算符時,智能提示將不會出現擴展方法(擴展方法前面有一個向下的箭頭標示出來的),下面是沒有注釋實例方法時智能提示的截圖(此時智能提示不會反射擴展方法出來):
並且從上面運行結果可以看出,當調用p.Print()方法時,此時調用的是離該調用較近的命名空間下的Print方法(盡管在CustomNamesapce命名空間下也定義了擴展方法Print)。、然而使用擴展方法還是存在一些問題的,如果同一個命名空間下的兩個類都含有擴展類型相同的方法時,此時編譯器就沒有辦法知道調用哪個方法了(這里標示出來引起大家的注意)。
三、在空引用上調用方法
大家都知道在C#中,在空引用上調用實例方法是會引發NullReferenceException異常的,但是可以在空引用上調用擴展方法,下面看一段演示代碼:
using System; namespace 在空引用上調用方法Demo { // 必須引入擴展方法定義的命名空間 using ExtensionDefine; class Program { static void Main(string[] args) { Console.WriteLine("空引用上調用擴展方法演示:"); string s = null; // 在該程序中要使用擴展方法必須通過using來引用 // 在空引用上調用擴展方法不會發生NullReferenceException異常 // 之所以不會出現異常,是因為在空引用上調用擴展方法,對於編譯器而言只是把空引用s當成參數傳入靜態方法中而已 // 對於編譯器來說,s.IsNull()的調用等效於下面的代碼 //Console.WriteLine("字符串S為空字符串:{0}", NullExten.IsNull(s)); Console.WriteLine("字符串S為空字符串:{0}", s.IsNull()); Console.ReadKey(); } } } namespace ExtensionDefine { /// <summary> /// 擴展方法定義 /// </summary> public static class NullExten { // 此時擴展的類型為object,這里我是故意用object類型的 // 如果是為了演示,當我們為一個類型定義擴展方法時,應盡量擴展具體類型,如果擴展其基類的話 // 則所有繼承於基類的類型都將具有該擴展方法,這樣對其他類型來說就進行了“污染 // 子所以形成了污染,是因為我們定義的擴展方法的意圖本來只想擴展某個子類。 // 其實下面這個方法我的意圖只是想擴展string類型的,所以更好的定義方法如下: //public static bool isNull(this string str) //{ // return str == null; //} // 不規范定義擴展方法的方式 public static bool IsNull(this object obj) { return obj == null; } } }
運行結果為:
在注釋中解釋了為什么在空引用中調用擴展方法不會拋出異常的原因,對於這個原因的解釋也不是我個人的猜測的,而是確實如此,其實用IL反匯編程序看看程序生成的中間代碼就可以證明了,下面Main函數中生成的中間代碼即IL(代碼中標注紅色的地方就是s.IsNull()的生成的IL代碼,代碼意思即是調用靜態類NullExten的靜態方法IsNull,此時只是把空引用s傳遞給該方法作為傳入參數,並不是真真在空引用中調用了方法。所以就不存在拋出異常了):
.method private hidebysig static void Main(string[] args) cil managed { .entrypoint // 代碼大小 43 (0x2b) .maxstack 2 .locals init ([0] string s) IL_0000: nop IL_0001: ldstr bytearray (7A 7A 15 5F 28 75 0A 4E 03 8C 28 75 69 62 55 5C // zz._(u.N..(uibU\ B9 65 D5 6C 14 6F 3A 79 1A FF ) // .e.l.o:y.. IL_0006: call void [mscorlib]System.Console::WriteLine(string) IL_000b: nop IL_000c: ldnull IL_000d: stloc.0 IL_000e: ldstr bytearray (57 5B 26 7B 32 4E 53 00 3A 4E 7A 7A 57 5B 26 7B // W[&{2NS.:NzzW[&{ 32 4E 1A FF 7B 00 30 00 7D 00 ) // 2N..{.0.}. IL_0013: ldloc.0 IL_0014: call bool ExtensionDefine.NullExten::IsNull(object) IL_0019: box [mscorlib]System.Boolean IL_001e: call void [mscorlib]System.Console::WriteLine(string, object) IL_0023: nop IL_0024: call valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey() IL_0029: pop IL_002a: ret } // end of method Program::Main
四、小結
到這里本專題的內容就介紹完了,這里總結下該專題介紹的內容:
- 介紹了擴展方法的定義和使用,以及擴展方法定義的規則,具體可以參照第一部分
- 介紹了編譯器是如何去發現擴展方法的,以及寫了一些例子進行測試,具體可以參照第二部分
- 解釋了為什么在空引用中可以調用擴展方法的原因,具體可以參照第三部分
在下一個專題將和大家介紹下C# 3中最重要的一個特性——Linq。
附上:程序中演示源碼:http://files.cnblogs.com/zhili/%E6%89%A9%E5%B1%95%E6%96%B9%E6%B3%95Demo.zip