.NET中那些所謂的新語法之二:匿名類、匿名方法與擴展方法


開篇:在上一篇中,我們了解了自動屬性、隱式類型、自動初始化器等所謂的新語法,這一篇我們繼續征程,看看匿名類、匿名方法以及常用的擴展方法。雖然,都是很常見的東西,但是未必我們都明白其中蘊含的奧妙。所以,跟着本篇的步伐,繼續來圍觀。

/* 新語法索引 */

5.匿名類 & 匿名方法
6.擴展方法

一、匿名類:[ C# 3.0/.NET 3.x 新增特性 ]

1.1 不好意思,我匿了

   在開發中,我們有時會像下面的代碼一樣聲明一個匿名類:可以看出,在匿名類的語法中並沒有為其命名,而是直接的一個new { }就完事了。從外部看來,我們根本無法知道這個類是干神馬的,也不知道它有何作用。

    var annoyCla1 = new
    {
        ID = 10010,
        Name = "EdisonChou",
        Age = 25
    };

    Console.WriteLine("ID:{0}-Name:{1}-Age:{2}", annoyCla1.ID,annoyCla1.Name, annoyCla1.Age);

  經過調試運行,我們發現匿名類完全可以實現具名類的效果:

1.2 深入匿名類背后

   既然我們發現匿名類可以完全實現具名類的效果,那么我們可以大膽猜測編譯器肯定在內部幫我們生成了一個類似具名類的class,於是,我們還是借助反編譯工具對其進行探索。通過Reflector反編譯,我們找到了編譯器生成的匿名類如下圖所示:

  從上圖可以看出:

  (1)匿名類被編譯后會生成一個[泛型類],可以看到上圖中的<>f__AnonymousType0<<ID>j__TPar, <Name>j__TPar, <Age>j__TPar>就是一個泛型類;

  (2)匿名類所生成的屬性都是只讀的,可以看出與其對應的字段也是只讀的;

  

  所以,如果我們在程序中為屬性賦值,那么會出現錯誤;

  

  (3)可以看出,匿名類還重寫了基類的三個方法:Equals,GetHashCode和ToString;我們可以看看它為我們所生成的ToString方法是怎么來實現的:

  

  實現的效果如下圖所示:

1.3 匿名類的共享

  可以想象一下,如果我們的代碼中定義了很多匿名類,那么是不是編譯器會為每一個匿名類都生成一個泛型類呢?答案是否定的,編譯器考慮得很遠,避免了重復地生成類型。換句話說,定義了多個匿名類的話如果符合一定條件則可以共享一個泛型類。下面,我們就來看看有哪幾種情況:

  (1)如果定義的匿名類與之前定義過的一模一樣:屬性類型和順序都一致,那么默認共享前一個泛型類

            var annoyCla1 = new
            {
                ID = 10010,
                Name = "EdisonChou",
                Age = 25
            };

            Console.WriteLine("ID:{0}-Name:{1}-Age:{2}", annoyCla1.ID,
                annoyCla1.Name, annoyCla1.Age);
            Console.WriteLine(annoyCla1.ToString());

            // 02.屬性類型和順序與annoyCla1一致,那么共同使用一個匿名類
            var annoyCla2 = new
                {
                    ID = 10086,
                    Name = "WncudChou",
                    Age = 25
                };
            Console.WriteLine("ID:{0}-Name:{1}-Age:{2}", annoyCla1.ID,
                annoyCla1.Name, annoyCla1.Age);
            Console.WriteLine("Is The Same Class of 1 and 2:{0}",
                annoyCla1.GetType() == annoyCla2.GetType());    

  通過上述代碼中的最后兩行:我們可以判斷其是否是一個類型?答案是:True

  (2)如果屬性名稱和順序一致,但屬性類型不同,那么還是共同使用一個泛型類,只是泛型參數改變了而已,所以在運行時會生成不同的類:

            var annoyCla3 = new
                {
                    ID = "EdisonChou",
                    Name = 10010,
                    Age = 25
                };
            Console.WriteLine("ID:{0}-Name:{1}-Age:{2}", annoyCla3.ID,
                annoyCla3.Name, annoyCla3.Age);
            Console.WriteLine("Is The Same Class of 2 and 3:{0}",
                annoyCla3.GetType() == annoyCla2.GetType());

  我們剛剛說到雖然共享了同一個泛型類,只是泛型參數改變了而已,所以在運行時會生成不同的類。所以,那么可以猜測到最后兩行代碼所顯示的結果應該是False,他們雖然都使用了一個泛型類,但是在運行時生成了兩個不同的類。

  (3)如果數據型名稱和類型相同,但順序不同,那么編譯器會重新創建一個匿名類

            var annoyCla4 = new
                {
                    Name = "EdisonChou",
                    ID = 10010,
                    Age = 25
                };
            Console.WriteLine("ID:{0}-Name:{1}-Age:{2}", annoyCla4.ID,
                annoyCla4.Name, annoyCla4.Age);
            Console.WriteLine("Is The Same Class of 2 and 4:{0}",
                annoyCla4.GetType() == annoyCla2.GetType());

  運行判斷結果為:False

  通過Reflector,可以發現,編譯器確實重新生成了一個泛型類:

二、匿名方法:[ C# 2.0/.NET 2.0 新增特性 ]

2.1 從委托的聲明說起

  C#中的匿名方法是在C#2.0引入的,它終結了C#2.0之前版本聲明委托的唯一方法是使用命名方法的時代。不過,這里我們還是看一下在沒有匿名方法之前,我們是如何聲明委托的。

  (1)首先定義一個委托類型:

public delegate void DelegateTest(string testName);

  (2)編寫一個符合委托規定的命名方法:

        public void TestFunc(string name)
        {
            Console.WriteLine("Hello,{0}", name);
        }

  (3)最后聲明一個委托實例:

    DelegateTest dgTest = new DelegateTest(TestFunc);
    dgTest("Edison Chou");

  (4)調試運行可以得到以下輸出:

  由上面的步湊可以看出,我們要聲明一個委托實例要為其編寫一個符合規定的命名方法。但是,如果程序中這個方法只被這個委托使用的話,總會感覺代碼結構有點浪費。於是,微軟引入了匿名方法,使用匿名方法聲明委托,就會使代碼結構變得簡潔,也會省去實例化的一些開銷。

2.2 引入匿名方法

  (1)首先,我們來看看上面的例子如何使用匿名方法來實現:

DelegateTest dgTest2 = new DelegateTest(delegate(string name)
{
      Console.WriteLine("Good,{0}", name);
});

從運行結果圖中可以看出,原本需要傳遞方法名的地方我們直接傳遞了一個方法,這個方法以delegate(參數){方法體}的格式編寫,在{}里邊直接寫了方法體內容。於是,我們不禁歡呼雀躍,又可以簡化一些工作量咯!

  (2)其次,我們將生成的程序通過Reflector反編譯看看匿名方法是怎么幫我們實現命名方法的效果的。

  ①我們可以看到,在編譯生成的類中,除了我們自己定義的方法外,還多了兩個莫名其妙的成員:

  ②經過一一查看,原來編譯器幫我們生成了一個私有的委托對象以及一個私有的靜態方法。我們可以大膽猜測:原來匿名方法不是沒有名字的方法,還是生成了一個有名字的方法,只不過這個方法的名字被藏匿起來了,而且方法名是編譯器生成的。

  ③經過上面的分析,我們還是不甚了解,到底匿名方法委托對象在程序中是怎么體現的?這里,我們需要查看Main方法,但是通過C#代碼我們沒有發現一點可以幫助我們理解的。這時,我們想要刨根究底就有點麻煩了。還好,在高人指點下,我們知道可以借助IL(中間代碼)來分析一下。於是,在Reflector中切換展示語言,將C#改為IL,就會看到另外一番天地。

  (3)由上面的分析,我們可以做出結論:編譯器對於匿名方法幫我們做了兩件事,一是生成了一個私有靜態的委托對象和一個私有靜態方法;二是將生成的方法的地址存入了委托,在運行時調用委托對象的Invoke方法執行該委托對象所持有的方法。因此,我們也可以看出,匿名方法需要結合委托使用

2.3 匿名方法擴展

  (1)匿名方法語法糖—更加簡化你的代碼

  在開發中,我們往往會采用語法糖來寫匿名方法,例如下面所示:

        DelegateTest dgTest3 = delegate(string name)
        {
           Console.WriteLine("Goodbye,{0}", name);
        };
        dgTest3("Edison Chou");

  可以看出,使用該語法糖,將new DelegateTest()也去掉了。可見,編譯器讓我們越來越輕松了。

  (2)傳參也有大學問—向方法中傳入匿名方法作為參數

  ①在開發中,我們往往聲明了一個方法,其參數是一個委托對象,可以接受任何符合委托定義的方法。

    static void InvokeMethod(DelegateTest dg)
    {
         dg("Edison Chou");
    }

  ②我們可以將已經定義的方法地址作為參數傳入InvokeMethod方法,例如:InvokeMethod(TestFunc); 當然,我們也可以使用匿名方法,不需要單獨定義就可以調用InvokeMethod方法。

    InvokeMethod(delegate(string name)
    {
          Console.WriteLine("Fuck,{0}", name);
    });

  (3)省略省略再省略—省略"大括號"

  經過編譯器的不斷優化,我們發現連delegate后邊的()都可以省略了,我們可以看看下面一段代碼:

    InvokeMethod(delegate { 
         Console.WriteLine("I love C sharp!"); 
    });

  而我們之前的定義是這樣的:

        public delegate void DelegateTest(string testName);

        static void InvokeMethod(DelegateTest dg)
        {
            dg("Edison Chou");
        }

  我們發現定義時方法是需要傳遞一個string類型的參數的,但是我們省略了deletegate后面的括號之后就沒有參數了,那么結果又是什么呢?經過調試,發現結果輸出的是:I love C sharp!

  這時,我們就有點百思不得其解了!明明都沒有定義參數,為何還是滿足了符合委托定義的參數條件呢?於是,我們帶着問題還是借助Reflector去一探究竟。

  ①在Main函數中,可以看到編譯器為我們自動加上了符合DelegateTest這個委托定義的方法參數,即一個string類型的字符串。雖然,輸出的是I love C sharp,但它確實是符合方法定義的,因為它會接受一個string類型的參數,盡管在方法體中沒有使用到這個參數。

  ②剛剛在Main函數中看到了匿名方法,現在可以看看編譯器為我們所生成的命名方法。

三、擴展方法:[ C# 3.0/.NET 3.x 新增特性 ]

3.1 神奇—初玩擴展方法

  (1)提到擴展方法,我想大部分的園友都不陌生了。不過還是來看看MSDN的定義:

MSDN 說:擴展方法使您能夠向現有類型“添加”方法,而無需創建新的派生類型、重新編譯或以其他方式修改原始類型。這里的“添加”之所以使用引號,是因為並沒有真正地向指定類型添加方法。

  那么,有時候我們會問:為什么要有擴展方法呢?這里,我們可以顧名思義地想一下,擴展擴展,那么肯定是涉及到可擴展性。在抽象工廠模式中,我們可以通過新增一個工廠類,而不需要更改源代碼就可以切換到新的工廠。這里也是如此,在不修改源碼的情況下,為某個類增加新的方法,也就實現了類的擴展。

  (2)空說無憑,我們來看看在C#中是怎么來判斷擴展方法的:通過智能提示,我們發現有一些方法帶了一個指向下方的箭頭,查看“溫馨提示”,我們知道他是一個擴展方法。所得是乃,原來我們一直對集合進行篩選的Where()方法居然是擴展方法而不是原生的。

  我們再來看看使用Where這個擴展方法的代碼示例:

        static void UseExtensionMethod()
        {
            List<Person> personList = new List<Person>()
            {
                new Person(){ID=1,Name="Big Yellow",Age=10},
                new Person(){ID=2,Name="Little White",Age=15},
                new Person(){ID=3,Name="Middle Blue",Age=7}
            };

            // 下面就使用了IEnumerable的擴展方法:Where
            var datas = personList.Where(delegate(Person p)
            {
                return p.Age >= 10;
            });

            foreach (var data in datas)
            {
                Console.WriteLine("{0}-{1}-{2}", 
                    data.ID, data.Name, data.Age);
            }
        }

  上述代碼使用了Where擴展方法,找出集合中Age>=10的數據形成新的數據集並輸出:

  (3)既然擴展方法是為了對類進行擴展,那么我們可不可以進行自定義擴展呢?答案是必須可以。我們先來看看擴展方法是如何的定義的,可以通過剛剛的IEnumerable接口中的Where方法定義來看看有哪些規則:通過 轉到定義 的方式,我們可以看到在System.Linq命名空間下,有叫做Enumerable的這樣一個靜態類,它的成員方法全是靜態方法,而且每個方法的大部分第一參數都是以this開頭。於是,我們可以總結出,擴展方法的三個要素是:靜態類靜態方法以及this關鍵字

    public static class Enumerable
    {
        public static IEnumerable<TSource> Union<TSource>(this IEnumerable<TSource> first, IEnumerable<TSource> second, IEqualityComparer<TSource> comparer);
    }

  那么問題又來了:為何一定得是static靜態的呢?這個我們都知道靜態方法是不屬於某個類的實例的,也就是說我們不需要實例化這個類,就可以訪問這個靜態方法。所以,你懂的啦。

  (4)看完擴展方法三要素,我們就來自動動手寫一個擴展方法:

    public static class PersonExtension
    {
        public static string FormatOutput(this Person p)
        {
            return string.Format("ID:{0},Name:{1},Age:{2}",
                p.ID, p.Name, p.Age);
        }
    }

  上面這個擴展方法完成了一個格式化輸出Person對象屬性信息的字符串構造,可以完成上面例子中的輸出效果。於是,我們可以將上面的代碼改為以下的方式進行輸出:

        static void UseMyExtensionMethod()
        {
            List<Person> personList = new List<Person>()
            {
                new Person(){ID=1,Name="Big Yellow",Age=10},
                new Person(){ID=2,Name="Little White",Age=15},
                new Person(){ID=3,Name="Middle Blue",Age=7}
            };

            var datas = personList.Where(delegate(Person p)
            {
                return p.Age >= 10;
            });

            foreach (var data in datas)
            {
                Console.WriteLine(data.FormatOutput());
            }
        }

3.2 嗦嘎—探秘擴展方法

  剛剛我們體驗了擴展方法的神奇之處,現在我們本着刨根究底的學習態度,借助Reflector看看編譯器到底幫我們做了什么工作?

  (1)通過反編譯剛剛那個UseMyExtensionMethod方法,我們發現並沒有什么奇怪之處。

  (2)這時,我們可以將C#切換到IL代碼看看,或許會有另一番收獲?於是,果斷切換之后,發現了真諦!

  原來編譯器在編譯時自動將Person.FormatOutput更改為了PersonExtension.FormatOutput,這時我們仿佛茅塞頓開,所謂的擴展方法,原來就是靜態方法的調用而已,所德是乃(原來如此)!於是,我們可以將這樣認為:person.FormatOutput() 等同於調用 PersonExtension.FormatOutput(person);

  (3)再查看所編譯生成的方法,發現this關鍵已經消失了。我們不禁一聲感嘆,原來this只是一個標記而已,標記它是擴展的是哪一個類型,在方法體中可以對這個類型的實例進行操作。

3.3 注意—總結擴展方法

  (1)如何定義擴展方法:

  定義靜態類,並添加public的靜態方法,第一個參數 代表 擴展方法的擴展類。

  a) 它必須放在一個非嵌套、非泛型的靜態類中(的靜態方法);

  b) 它至少有一個參數;

  c) 第一個參數必須附加 this 關鍵字;

  d) 第一個參數不能有任何其他修飾符(out/ref)

  e) 第一個參數不能是指針類型

  (2)當我們把擴展方法定義到其它程序集中時,一定要注意調用擴展方法的環境中需要包含擴展方法所在的命名空間

  (3)如果要擴展的類中本來就有和擴展方法的名稱一樣的方法,到底會調用成員方法還是擴展方法呢?

答案:編譯器默認認為一個表達式是要使用一個實例方法,但如果沒有找到,就會檢查導入的命名空間和當前命名空間里所有的擴展方法,並匹配到適合的方法。

參考文章

  (1)一線碼農,《來看看兩種好玩的方法:擴展方法和分部方法》:http://www.cnblogs.com/huangxincheng/p/4021192.html

  (2)Anders Cui,《擴展方法淺談》:http://www.cnblogs.com/anderslly/archive/2010/01/18/using-extension-methods.html

附件下載

  NewGrammerDemos v1.1 : http://pan.baidu.com/s/1pJsOrvd

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM