C# 泛型的協變和逆變

1. 可變性的類型:協變性和逆變性

可變性是以一種類型安全的方式,將一個對象當作另外一個對象來使用。若是不能將一個類型替換爲另外一個類型,那麼這個類型就稱之爲:不變量。協變和逆變是兩個相互對立的概念:安全

  • 若是某個返回的類型能夠由其派生類型替換,那麼這個類型就是支持協變
  • 若是某個參數類型能夠由其基類替換,那麼這個類型就是支持逆變的。

2. C# 4.0對泛型可變性的支持

在C# 4.0以前,全部的泛型類型都是不變量——即不支持將一個泛型類型替換爲另外一個泛型類型,即便它們之間擁有繼承關係,簡而言之,在C# 4.0以前的泛型都是不支持協變和逆變的。函數

C# 4.0經過兩個關鍵字:outin來分別支持以協變和逆變的方式使用泛型。單元測試

咱們來看一段利用了協變類型參數的代碼:測試

public class BaseClass
{
    //...
}

public class DerivedClass : BaseClass
{
    //...
}

下面咱們利用協變類型參數,能夠執行相似於普通的多態性的分配:設計

IEnumerable<DerivedClass> d = new List<DerivedClass>();
IEnumerable<BaseClass> b = d;

在上面的實例中,在C# 4.0以前是不能正常編譯的,除了對賦值給基類集合時將子類集合作一個強制轉換,可是在運行時仍然會拋出一個類型轉換的異常。code

下面咱們再看一個關於逆變的實例代碼:對象

Action<BaseClass> b = (target) => { Console.WriteLine(target.GetType().Name); };
Action<DerivedClass> d = b;
d(new DerivedClass());

在上面的示例中咱們 Action<BaseClass> 類型的委託分配給類型 Action<DerivedClass> 的變量,根據逆變的定義咱們能夠知道 Action<T> 類型是支持逆變的。繼承

爲何IEnumerable<T>Action<T> 能夠分別支持類型的協變和逆變呢?咱們查看這兩個類型在 .NET 中的定義:接口

//IEnumerable<T> 接口的定義(支持協變)
public interface IEnumerable<out T> : IEnumerable

//Action<T> 委託的定義(支持逆變)
public delegate void Action<in T>(T obj);

爲了保證類型的安全,C#編譯器對使用了 outin 關鍵字的泛型參數添加了一些限制:開發

  • 支持協變(out)的類型參數只能用在輸出位置:函數返回值、屬性的get訪問器以及委託參數的某些位置
  • 支持逆變(in)的類型參數只能用在輸入位置:方法參數或委託參數的某些位置中出現。

3. C#中泛型可變性的限制

1. 不支持類的類型參數的可變性

只有接口和委託能夠擁有可變的類型參數。inout 修飾符只能用來修飾泛型接口和泛型委託。

2. 可變性只支持引用轉換

可變性只能用於引用類型,禁止任何值類型和用戶定義的轉換,以下面的轉換是無效的:

  • IEnumerable<int> 轉換爲 IEnumerable<object> ——裝箱轉換
  • IEnumerable<short> 轉換爲 IEnumerable<int> ——值類型轉換
  • IEnumerable<string> 轉換爲 IEnumerable<XName> ——用戶定義的轉換

3. 類型參數使用了 out 或者 ref 將禁止可變性

對於泛型類型參數來講,若是要將該類型的實參傳給使用 out 或者 ref 關鍵字的方法,便不容許可變性,如:

delegate void someDelegate<in T>(ref T t)

這段代碼編譯器會報錯。

4. 可變性必須顯式指定

從實現上來講編譯器徹底能夠本身判斷哪些泛型參數可以逆變和協變,但實際卻沒有這麼作,這是由於C#的開發團隊認爲:

必須由開發者明確的指定可變性,由於這會促使開發者考慮他們的行爲將會帶來什麼後果,從而思考他們的設計是否合理。

5. 注意破壞性修改

在修改已有代碼接口的可變性時,會有破壞當前代碼的風險。例如,若是你依賴於不容許可變性的is或as操做符的結果,運行在.NET 4時,代碼的行爲將有所不一樣。一樣,在某些狀況下,由於有了更多可用的選項,重載決策也會選擇不一樣的方法。因此在對已有代碼引入可變性時要作好足夠的單元測試以及防護措施。

6. 多播委託與可變性不能混用

下面的代碼可以經過編譯,可是在運行時會拋出 ArgumentException 異常:

Func<string> stringFunc = () => "";
Func<object> objectFunc = () => new object();
Func<object> combined = objectFunc + stringFunc;

這是由於負責連接多個委託的 Delegate.Combine方法要求參數必須爲相同的類型。上面的示例咱們能夠修改爲以下正確的代碼:

Func<string> stringFunc = () => "";
Func<object> defensiveCopy = new Func<object>(stringFunc);
Func<object> objectFunc = () => new object();
Func<object> combined = objectFunc + defensiveCopy;

參考&擴展閱讀

協變和逆變
泛型中的協變和逆變
委託中的協變和逆變
《深刻理解C#》:13.3 接口和委託的泛型可變性
《Effective C#》:條目29:支持泛型協變和逆變
《CLR via C#》:12.5 委託和接口的逆變和協變泛型類型實參

相關文章
相關標籤/搜索