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