C#.NET:高階程式設計之匿名類、匿名方法與擴充套件方法
[文中插圖丟失,推薦檢視原文]!important
開篇:在上一篇中,我們瞭解了自動屬性、隱式型別、自動初始化器等所謂的新語法,這一篇我們繼續征程,看看匿名類、匿名方法以及常用的擴充套件方法。雖然,都是很常見的東西,但是未必我們都明白其中蘊含的奧妙。所以,跟著本篇的步伐,繼續來圍觀。
/* 新語法索引 */
5.匿名類 &匿名方法
6.擴充套件方法
一、匿名類:[ C# 3.0/.NET3.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 loveC 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)如果要擴充套件的類中本來就有和擴充套件方法的名稱一樣的方法,到底會呼叫成員方法還是擴充套件方法呢?
答案:編譯器預設認為一個表示式是要使用一個例項方法,但如果沒有找到,就會檢查匯入的名稱空間和當前名稱空間裡所有的擴充套件方法,並匹配到適合的方法。
參考文章
附件下載
作者:周旭龍
本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連結。