C# 泛型的協變和逆變

發表於2015-05-04

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

可變性是以一種型別安全的方式,將一個物件當做另一個物件來使用。如果不能將一個型別替換為另一個型別,那麼這個型別就稱之為:不變數。協變和逆變是兩個相互對立的概念:

  • 如果某個返回的型別可以由其派生型別替換,那麼這個型別就是支援協變的
  • 如果某個引數型別可以由其基類替換,那麼這個型別就是支援逆變的。

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

在C# 4.0之前,所有的泛型型別都是不變數——即不支援將一個泛型型別替換為另一個泛型型別,即使它們之間擁有繼承關係,簡而言之,在C# 4.0之前的泛型都是不支援協變和逆變的。

C# 4.0通過兩個關鍵字:out和in來分別支援以協變和逆變的方式使用泛型。

我們來看一段利用了協變型別引數的程式碼:

 

下面我們利用協變型別引數,可以執行類似於普通的多型性的分配:

在上面的例項中,在C# 4.0之前是不能正常編譯的,除了對賦值給基類集合時將子類集合做一個強制轉換,但是在執行時仍然會丟擲一個型別轉換的異常。

下面我們再看一個關於逆變的例項程式碼:

在上面的示例中我們 Action<BaseClass> 型別的委託分配給型別 Action<DerivedClass> 的變數,根據逆變的定義我們可以知道 Action<T> 型別是支援逆變的。

為什麼IEnumerable<T> 和 Action<T> 可以分別支援型別的逆變和協變呢?我們檢視這兩個型別在 .NET 中的定義:

為了保證型別的安全,C#編譯器對使用了 out 和 in 關鍵字的泛型引數新增了一些限制:

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

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

1. 不支援類的型別引數的可變性

只有介面和委託可以擁有可變的型別引數。in 和 out 修飾符只能用來修飾泛型介面和泛型委託。

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

可變性只能用於引用型別,禁止任何值型別和使用者定義的轉換,如下面的轉換是無效的:

  • 將 IEnumerable<int> 轉換為 IEnumerable<object> ——裝箱轉換
  • 將 IEnumerable<short> 轉換為 IEnumerable<int> ——值型別轉換
  • 將 IEnumerable<string> 轉換為 IEnumerable<XName> ——使用者定義的轉換

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

對於泛型型別引數來說,如果要將該型別的實參傳給使用 out 或者 ref 關鍵字的方法,便不允許可變性,如:

這段程式碼編譯器會報錯。

4. 可變性必須顯式指定

從實現上來說編譯器完全可以自己判斷哪些泛型引數能夠逆變和協變,但實際卻沒有這麼做,這是因為C#的開發團隊認為:

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

5. 注意破壞性修改

在修改已有程式碼介面的可變性時,會有破壞當前程式碼的風險。例如,如果你依賴於不允許可變性的is或as操作符的結果,執行在.NET 4時,程式碼的行為將有所不同。同樣,在某些情況下,因為有了更多可用的選項,過載決策也會選擇不同的方法。所以在對已有程式碼引入可變性時要做好足夠的單元測試以及防禦措施。

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

下面的程式碼能夠通過編譯,但是在執行時會丟擲 ArgumentException 異常:

這是因為負責連結多個委託的 Delegate.Combine方法要求引數必須為相同的型別。上面的示例我們可以修改成如下正確的程式碼:

參考&擴充套件閱讀

協變和逆變
泛型中的協變和逆變
委託中的協變和逆變
《深入理解C#》:13.3 介面和委託的泛型可變性
《Effective C#》:條目29:支援泛型協變和逆變
《CLR via C#》:12.5 委託和介面的逆變和協變泛型型別實參

相關文章