VS2010的推出會為我們帶來新版本的C#。了解C#4.0中的新功能有助於我們利用編碼。它還能夠幫助我們了解程序中正在出現,而下一代的C#有可能會解決的錯誤。最終,這樣的實踐可以幫助我們在現有的知識結構上創建適應C#4.0的業務。
在本文中我們關注的是C# 4.0中的協變性和逆變性。
恆定性,協變性和逆變性
在進一步研究問題之前,我們先解釋一下恆定性,協變性,逆變性參數以及返回類型這些概念的意思。大家對這些概念應該是熟悉的,即便那你可能並不能把握這些概念的正式定義。
如果你必須使用完全匹配正式類型的名稱,那麼返回的值或參數是不變的。如果你能夠使用更多的衍生類型作為正式參數類型的代替物,那麼參數是可變的。如果你能夠將返回的類型分配給擁有較少類型的變量,那麼返回的值是逆變的。
在大多數情況下,C#支持協變參數和逆變的返回類型。這一特性也符合其他所有的對象指向型語言。事實上,多態性通常是建立在協變和逆變的概念之上的。直觀上,我們發現是可以將衍生的類對象發送給任何期望基類對象的方法。比較,衍生的對象也是基類對象的實例。本能地我們也清楚,我們可以將方法的結果保存在擁有較少衍生對象類型的變量中。例如,你可能會需要對這段代碼進行編譯:
public static void PrintOutput(object thing)
{
if (thing != null)
Console.WriteLine(thing);
}
// elsewhere:
PrintOutput(5);
PrintOutput("This is a string");
這段代碼之所以有效是因為參數類型在C#中具有協變性,你可以將任意方法保存在類型對象的變量中,因為C#中返回類型是逆變的:
object value = SomeMethod();
如果在.NET推出後,你已經了解C#或VB.NET,那麼你應該很熟悉以上的內容。但是規則發生了一些改變。在很多方法中,你直覺上認為有效的其實不然。隨著你漸漸深入了解,會發現你曾經認為是漏洞的東西很可能是該語言的說明。現在是時候解釋一下為什麼集合以不同的方式工作,以及未來將發生些什麼變化。
基於對象的集合
.NET 1.x集合(ArrayList,HashTable,Queue等)可以被視為具有協變性。遺憾的是,它們不具有安全的協變性。事實上,它們具有恆定性。不過由於它們向System.Object保存了參考,它們看上去像是具有了協變性和逆變性。舉幾個例子就可以說明這個問題。
你可以認為這些集合是協變的,因為你可以創建一個員工對象的數組列表,然後使用這個列表作為任意方法的參數,這些方法使用的是類型數組列表的對象。通常這種方法很有效。這個方法可能能夠與數組列表連用:
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);
}
這個方法是安全的因為它沒有改變集合中任何對象的類型。它列舉了集合並將集合中已有的項目移動到了不同索引。不過並未改變任何類型,因此這個方法適用於所有實例。但是數組列表和其他傳統的.NET 1.x集合不會被視為安全的協變。看這一方法:
private void UnsafeUse(ArrayList stuff)
{
for (int index = 0; index < stuff.Count; index++)
stuff[index] = stuff[index].ToString();
}
這是對保存在集合中的作出的更深一層的假設。當方法存在時候,集合包含了類型字符串的對象。或許這不再是原始集合中的類型。事實上,如果原始集合包含這些字符串,那麼方法就不會產生效果。否則,它會將集合轉換為不同的類型。下列使用實例顯示了在調用方法的時候遇到的各種問題。此處,一列數字被發送到了UnsafeUse,而數字正是在此處被轉換成了字符串的數組列表。調用以後,呼叫代碼會嘗試再一次創建能夠導致InvalidCastException的項目。
// 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");
}
}
這個例子表明雖然典型的集合是不變的,但是你可以視它們為可變或可逆變。不過這些集合並非安全可變。編譯器難保不會出現失誤。
數組
作為參數使用的時候,數組時而可變時而不可變。和典型集合一樣,數組具有非安全的協變性。首先,只有包含了參考類型的數組可以被視為具有協變性或逆變性。值類型的數組通常不可變,即便是調用一個期望對象數組的方法時也是如此。這一方法可以與其他任何參考類型的數組一起調用,但是你不能向其發送整數數組或其他數值類型:
private void PrintCollection(object[] collection)
{
foreach (object o in collection)
Console.WriteLine(o);
}
只要你限制引用類型,數組就會具有協變性和逆變性。但是仍然是不安全的。你將數組視為可變或逆變的次數越多,越會發現你需要處理ArrayTypeMismatchException。讓我們檢查其中的一些方法。數組參數是可變的,但卻是非安全協變。檢查下列不安全的方法:
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");
}
}
下面的調用順序會引發循環以拋出一個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 對象數組,然後調用了期望B對象數組的方法。因為數組是可變的,你可以將D[]發送到期望B[]的方法。但是在DestroyCollection()裡面,可以修改數組。在本例中,它創建了用於集合的新對象,類型D2的對象。這在該方法中是允許的:D2對象可以保存在B[]中因為D2是由B衍生出來的。但是其結合往往會引發錯誤。當你引入一些返回數組儲存的方法並視其為逆變值時,同樣的事情也會發生。向這樣的代碼才能有效:
B[] storage = GenerateCollection();
storage[0] = new B();
但是,如果GenerateCollection的內容向這樣的話,那麼當storage[0]要素被設置到B對象中,它會引發ArrayTypeMismatch異常。
泛型集合
數組被當作是可變和可逆變,即便是不安全的。.NET1.x集合類型是不可變的,但是將參考保存到了Systems.Object。.NET2.x中的泛型集合並且被視為不可變。這意味著你不能夠替代包含有較多衍生對象的集合。最好你試一試下面的代碼:
private void WriteItems(IEnumerable< object> sequence)
{
foreach (var item in sequence)
Console.WriteLine(item);
}
你要知道自己可能會和其他執行IEnumberable< T>集合一起對其進行調用因為任何T必須由對象衍生。這或許是你的期望,但是由於泛型是不變的,下面的操作將無法進行編譯:
IEnumerable< int> items = Enumerable.Range(1, 50);
WriteItems(items); // generates CS1502, CS1503
你也不能將泛型集合類型視為可逆變。這行代碼之所以不能進行編譯是因為分配返回數值的時候,你不能將IEnumberable< T>轉換成IEnumberable< object>:
IEnumerable< object> moreItems =
Enumerable.Range(1, 50);
你或許認為IEnumberable< int>衍生自IEnumberable< object>,但是事實不然。IEnumberable< int>是一個基於IEnumberable< T>泛型類定義的閉合泛型類。它們不會相互衍生,因此沒有關聯性,而且你也不能視其具有可變性。即便在兩個類型參數之間具備關聯性,使用類型參數的泛型類型不會對這種關聯有響應。
C#以不變的方式對待泛型顯示出了該語言的強大優勢。最重要的是,你不能在數組和1.x集合中出錯。一旦你編譯了泛型代碼,你就能夠很好地利用這些代碼了。這與C#的傳統具有一致性,因為它利用了編譯器來刪除代碼中可能存在的漏洞。
但是對於對於強效輸入的依賴性顯示出了一定的局限性。上文顯示的關於泛型轉換的構造看上去是有效的。但是你不會想將其轉換為.NET1.x集合和數組中使用的行為。我們真正想要的是僅在它運行的時候將泛型類型視作是可變的或可逆變的,而不是用運行時錯誤代替編譯時錯誤的時候。