C# 4.0中泛型協(xié)變性和逆變性詳解
VS2010的推出會(huì)為我們帶來新版本的C#。了解C#4.0中的新功能有助于我們利用編碼。它還能夠幫助我們了解程序中正在出現(xiàn),而下一代的C#有可能會(huì)解決的錯(cuò)誤。最終,這樣的實(shí)踐可以幫助我們?cè)诂F(xiàn)有的知識(shí)結(jié)構(gòu)上創(chuàng)建適應(yīng)C#4.0的業(yè)務(wù)。
在本文中我們關(guān)注的是C# 4.0中的協(xié)變性和逆變性。
恒定性,協(xié)變性和逆變性
在進(jìn)一步研究問題之前,我們先解釋一下恒定性,協(xié)變性,逆變性參數(shù)以及返回類型這些概念的意思。大家對(duì)這些概念應(yīng)該是熟悉的,即便那你可能并不能把握這些概念的正式定義。
如果你必須使用完全匹配正式類型的名稱,那么返回的值或參數(shù)是不變的。如果你能夠使用更多的衍生類型作為正式參數(shù)類型的代替物,那么參數(shù)是可變的。如果你能夠?qū)⒎祷氐念愋头峙浣o擁有較少類型的變量,那么返回的值是逆變的。
在大多數(shù)情況下,C#支持協(xié)變參數(shù)和逆變的返回類型。這一特性也符合其他所有的對(duì)象指向型語言。事實(shí)上,多態(tài)性通常是建立在協(xié)變和逆變的概念之上的。直觀上,我們發(fā)現(xiàn)是可以將衍生的類對(duì)象發(fā)送給任何期望基類對(duì)象的方法。比較,衍生的對(duì)象也是基類對(duì)象的實(shí)例。本能地我們也清楚,我們可以將方法的結(jié)果保存在擁有較少衍生對(duì)象類型的變量中。例如,你可能會(huì)需要對(duì)這段代碼進(jìn)行編譯:
public static void PrintOutput(object thing) { if (thing != null) Console.WriteLine(thing); } // elsewhere: PrintOutput(5); PrintOutput("This is a string"); |
這段代碼之所以有效是因?yàn)閰?shù)類型在C#中具有協(xié)變性,你可以將任意方法保存在類型對(duì)象的變量中,因?yàn)镃#中返回類型是逆變的:
object value = SomeMethod(); |
如果在.NET推出后,你已經(jīng)了解C#或VB.NET,那么你應(yīng)該很熟悉以上的內(nèi)容。但是規(guī)則發(fā)生了一些改變。在很多方法中,你直覺上認(rèn)為有效的其實(shí)不然。隨著你漸漸深入了解,會(huì)發(fā)現(xiàn)你曾經(jīng)認(rèn)為是漏洞的東西很可能是該語言的說明?,F(xiàn)在是時(shí)候解釋一下為什么集合以不同的方式工作,以及未來將發(fā)生些什么變化。
基于對(duì)象的集合
.NET 1.x集合(ArrayList,HashTable,Queue等)可以被視為具有協(xié)變性。遺憾的是,它們不具有安全的協(xié)變性。事實(shí)上,它們具有恒定性。不過由于它們向System.Object保存了參考,它們看上去像是具有了協(xié)變性和逆變性。舉幾個(gè)例子就可以說明這個(gè)問題。
你可以認(rèn)為這些集合是協(xié)變的,因?yàn)槟憧梢詣?chuàng)建一個(gè)員工對(duì)象的數(shù)組列表,然后使用這個(gè)列表作為任意方法的參數(shù),這些方法使用的是類型數(shù)組列表的對(duì)象。通常這種方法很有效。這個(gè)方法可能能夠與數(shù)組列表連用:
private void SafeCovariance(ArrayList bunchOfItems) { foreach(object o in bunchOfItems) Console.WriteLine(o); // reverse the items: int start = 0; int end = bunchOfItems.Count - 1; while (start < end) { object tmp = bunchOfItems[start]; bunchOfItems[start] = bunchOfItems[end]; bunchOfItems[end] = tmp; start++; end--; } foreach(object o in bunchOfItems) Console.WriteLine(o); } |
這個(gè)方法是安全的因?yàn)樗鼪]有改變集合中任何對(duì)象的類型。它列舉了集合并將集合中已有的項(xiàng)目移動(dòng)到了不同索引。不過并未改變?nèi)魏晤愋?,因此這個(gè)方法適用于所有實(shí)例。但是數(shù)組列表和其他傳統(tǒng)的.NET 1.x集合不會(huì)被視為安全的協(xié)變??催@一方法:
private void UnsafeUse(ArrayList stuff) { for (int index = 0; index < stuff.Count; index++) stuff[index] = stuff[index].ToString(); } |
這是對(duì)保存在集合中的作出的更深一層的假設(shè)。當(dāng)方法存在時(shí)候,集合包含了類型字符串的對(duì)象?;蛟S這不再是原始集合中的類型。事實(shí)上,如果原始集合包含這些字符串,那么方法就不會(huì)產(chǎn)生效果。否則,它會(huì)將集合轉(zhuǎn)換為不同的類型。下列使用實(shí)例顯示了在調(diào)用方法的時(shí)候遇到的各種問題。此處,一列數(shù)字被發(fā)送到了UnsafeUse,而數(shù)字正是在此處被轉(zhuǎn)換成了字符串的數(shù)組列表。調(diào)用以后,呼叫代碼會(huì)嘗試再一次創(chuàng)建能夠?qū)е翴nvalidCastException的項(xiàng)目。
// usage: public void DoTest() { ArrayList collection = new ArrayList() { 1,2,3,4,5,6,7, 8, 9, 10, 11,12,13,14,15,16,17,18,19,20, 21,22,23,24,25,26,27,28,29,30 }; SafeCovariance(collection); // create the sum: int sum = 0; foreach (int num in collection) sum += num; Console.WriteLine(sum); UnsafeUse(collection); // create the sum: sum = 0; try { foreach (int num in collection) sum += num; Console.WriteLine(sum); } catch (InvalidCastException) { Console.WriteLine( "Not safely covariant"); } } |
這個(gè)例子表明雖然典型的集合是不變的,但是你可以視它們?yōu)榭勺兓蚩赡孀?。不過這些集合并非安全可變。編譯器難保不會(huì)出現(xiàn)失誤。#p#
數(shù)組
作為參數(shù)使用的時(shí)候,數(shù)組時(shí)而可變時(shí)而不可變。和典型集合一樣,數(shù)組具有非安全的協(xié)變性。首先,只有包含了參考類型的數(shù)組可以被視為具有協(xié)變性或逆變性。值類型的數(shù)組通常不可變,即便是調(diào)用一個(gè)期望對(duì)象數(shù)組的方法時(shí)也是如此。這一方法可以與其他任何參考類型的數(shù)組一起調(diào)用,但是你不能向其發(fā)送整數(shù)數(shù)組或其他數(shù)值類型:
private void PrintCollection(object[] collection) { foreach (object o in collection) Console.WriteLine(o); } |
只要你限制引用類型,數(shù)組就會(huì)具有協(xié)變性和逆變性。但是仍然是不安全的。你將數(shù)組視為可變或逆變的次數(shù)越多,越會(huì)發(fā)現(xiàn)你需要處理ArrayTypeMismatchException。讓我們檢查其中的一些方法。數(shù)組參數(shù)是可變的,但卻是非安全協(xié)變。檢查下列不安全的方法:
private class B { public override string ToString() { return "This is a B"; } } private class D : B { public override string ToString() { return "This is a D"; } } private class D2 : B { public override string ToString() { return "This is a D2"; } } private void DestroyCollection(B[] storage) { try { for (int index = 0; index < storage.Length; index++) storage[index] = new D2(); } catch (ArrayTypeMismatchException) { Console.WriteLine("ArrayTypeMismatch"); } } |
下面的調(diào)用順序會(huì)引發(fā)循環(huán)以拋出一個(gè)ArrayTypeMismatch例外:
D[] array = new D[]{ new D(), new D(), new D(), new D(), new D(), new D(), new D(), new D(), new D(), new D()}; DestroyCollection(array); |
當(dāng)我們將兩個(gè)板塊集合起來看時(shí)就一目了然了。調(diào)用頁面創(chuàng)建了一個(gè)D 對(duì)象數(shù)組,然后調(diào)用了期望B對(duì)象數(shù)組的方法。因?yàn)閿?shù)組是可變的,你可以將D[]發(fā)送到期望B[]的方法。但是在DestroyCollection()里面,可以修改數(shù)組。在本例中,它創(chuàng)建了用于集合的新對(duì)象,類型D2的對(duì)象。這在該方法中是允許的:D2對(duì)象可以保存在B[]中因?yàn)镈2是由B衍生出來的。但是其結(jié)合往往會(huì)引發(fā)錯(cuò)誤。當(dāng)你引入一些返回?cái)?shù)組儲(chǔ)存的方法并視其為逆變值時(shí),同樣的事情也會(huì)發(fā)生。向這樣的代碼才能有效:
B[] storage = GenerateCollection(); storage[0] = new B(); |
但是,如果GenerateCollection的內(nèi)容向這樣的話,那么當(dāng)storage[0]要素被設(shè)置到B對(duì)象中,它會(huì)引發(fā)ArrayTypeMismatch異常。#p#
泛型集合
數(shù)組被當(dāng)作是可變和可逆變,即便是不安全的。.NET1.x集合類型是不可變的,但是將參考保存到了Systems.Object。.NET2.x中的泛型集合并且被視為不可變。這意味著你不能夠替代包含有較多衍生對(duì)象的集合。最好你試一試下面的代碼:
private void WriteItems(IEnumerable< object> sequence) { foreach (var item in sequence) Console.WriteLine(item); } |
你要知道自己可能會(huì)和其他執(zhí)行IEnumberable< T>集合一起對(duì)其進(jìn)行調(diào)用因?yàn)槿魏蜹必須由對(duì)象衍生。這或許是你的期望,但是由于泛型是不變的,下面的操作將無法進(jìn)行編譯:
IEnumerable< int> items = Enumerable.Range(1, 50);
WriteItems(items); // generates CS1502, CS1503
你也不能將泛型集合類型視為可逆變。這行代碼之所以不能進(jìn)行編譯是因?yàn)榉峙浞祷財(cái)?shù)值的時(shí)候,你不能將IEnumberable< T>轉(zhuǎn)換成IEnumberable< object>:
IEnumerable< object> moreItems =
Enumerable.Range(1, 50);
你或許認(rèn)為IEnumberable< int>衍生自IEnumberable< object>,但是事實(shí)不然。IEnumberable< int>是一個(gè)基于IEnumberable< T>泛型類定義的閉合泛型類。它們不會(huì)相互衍生,因此沒有關(guān)聯(lián)性,而且你也不能視其具有可變性。即便在兩個(gè)類型參數(shù)之間具備關(guān)聯(lián)性,使用類型參數(shù)的泛型類型不會(huì)對(duì)這種關(guān)聯(lián)有響應(yīng)。
C#以不變的方式對(duì)待泛型顯示出了該語言的強(qiáng)大優(yōu)勢(shì)。最重要的是,你不能在數(shù)組和1.x集合中出錯(cuò)。一旦你編譯了泛型代碼,你就能夠很好地利用這些代碼了。這與C#的傳統(tǒng)具有一致性,因?yàn)樗昧司幾g器來刪除代碼中可能存在的漏洞。
但是對(duì)于對(duì)于強(qiáng)效輸入的依賴性顯示出了一定的局限性。上文顯示的關(guān)于泛型轉(zhuǎn)換的構(gòu)造看上去是有效的。但是你不會(huì)想將其轉(zhuǎn)換為.NET1.x集合和數(shù)組中使用的行為。我們真正想要的是僅在它運(yùn)行的時(shí)候?qū)⒎盒皖愋鸵曌魇强勺兊幕蚩赡孀兊模皇怯眠\(yùn)行時(shí)錯(cuò)誤代替編譯時(shí)錯(cuò)誤的時(shí)候。
您正在閱讀:C# 4.0中泛型協(xié)變性和逆變性詳解
【編輯推薦】