天天看點

[C# 基礎知識系列]專題十五:全面解析擴充方法

引言: 

 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是嵌套類"的編譯時錯誤,示範代碼如下:

View Code  

        /// 擴充方法必須在非泛型靜态類中定義 

        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); 

            #endregion           

下面是出現編譯時錯誤截圖:

二、擴充方法是如何被發現的?

  從上面部分的介紹,朋友們應該知道了如何定義和使用一個擴充方法,并且從我們定義的規則中可以幫助我們開發人員更好地去識别擴充方法,知道程式中調用的是一個執行個體方法還是一個擴充方法,然而相信大家此時會有這樣一個疑問——編譯器是如何知道我調用的是一個擴充方法而不是一個該類中的一個執行個體方法呢?對于這個問題,将在這部分和大家分析下。

  首先讨論下程式員是如何去識别調用的是一個擴充方法而不是一個執行個體方法的,當我們看到調用方法的代碼時,首先我們會去找該方法是否是該類(如上面程式中的Stream類)的一個執行個體方法,進入Stream類(按F12進去檢視)的定義中卻發現該類中沒有一個名為CopyToNewStream的方法,此時我們就會檢視程式中是否定義了這樣的擴充方法,當找到一個為名CopyToNewStream這樣的方法時,然後再根據定義的規則來判斷找到的方法是否是為Stream類擴充的方法,這樣的一個過程就是我們程式員去發現一個擴充方法的過程,然而對于編譯器而言,它也是這麼去發現擴充方法的(進而可以看出C#編譯器還是非常智能的,完全按照人的思路去思考問題,因為它也是人實作出來的,就當然是盡可能地去以人的思考方式去實作的了),下面就介紹下編譯器是如何去發現擴充方法的,這樣也可以與程式員們的思路進行對比下。

  當編譯器看到變量調用的是一個方法時,它首先會去該對象中執行個體方法中去檢視,一旦沒有找到與調用方法同名的執行個體方法時,編譯器就會去查找一個合适的擴充方法,它會檢查導入的所有命名空間和目前的命名空間中的所有擴充方法,并比對變量類型到擴充類型存在一個隐式轉換的擴充方法。然而對于這個發現過程,可能有些人會問:編譯器如何知道某個方法是擴充方法而不是執行個體方法呢? 編譯器是根據System.Runtime.CompilerServices.ExtensionAttribute屬性來綁定方法是是否為擴充方法的, 當我們定義的方法是擴充方法時,該屬性會自動應用到方法上,編譯器還會将該特性應用到包含擴充方法的程式集上,對于這個兩點并不是我的推斷,下面給出反編譯截圖來證明下:

 從上面編譯器發現擴充方法的過程可以得到方法調用的優先級的結論:現有的執行個體方法——>目前命名空間下的擴充方法——>導入命名空間的擴充方法。下面通過一個例子來示範編譯器的發現過程:

using System; 

namespace 擴充方法如何被發現Demo 

    // 要使用不同命名空間的擴充方法首先要添加該命名空間的引用 

    using CustomNamesapce; 

    class Program 

            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 

        ///  擴充方法定義 

        /// <param name="per"></param> 

        public static void Print(this Person per) 

            Console.WriteLine("調用的是同一命名空間下的擴充方法輸出,姓名為: {0}", per.Name); 

namespace CustomNamesapce 

    using 擴充方法如何被發現Demo; 

    public static class CustomExtensionClass 

            Console.WriteLine("調用的是不同命名空間下擴充方法輸出,姓名為: {0}", per.Name); 

        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異常的,但是可以在空引用上調用擴充方法,下面看一段示範代碼:

namespace 在空引用上調用方法Demo 

    // 必須引入擴充方法定義的命名空間 

    using ExtensionDefine; 

            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 

    /// 擴充方法定義 

    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[&{ 

 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。

<a href="http://down.51cto.com/data/2361989" target="_blank">附件:http://down.51cto.com/data/2361989</a>

     本文轉自LearningHard 51CTO部落格,原文連結:http://blog.51cto.com/learninghard/1092482,如需轉載請自行聯系原作者

繼續閱讀