歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux編程 >> Linux編程 >> C# 泛型的協變和逆變

C# 泛型的協變和逆變

日期:2017/3/1 9:29:43   编辑:Linux編程

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之前是不能正常編譯的,除了對賦值給基類集合時將子類集合做一個強制轉換,但是在運行時仍然會拋出一個類型轉換的異常。

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

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#多線程編程實例 線程與窗體交互【附源碼】 http://www.linuxidc.com/Linux/2014-07/104294.htm

C#數學運算表達式解釋器 http://www.linuxidc.com/Linux/2014-07/104289.htm

在C語言中解析JSON配置文件 http://www.linuxidc.com/Linux/2014-05/101822.htm

C++ Primer Plus 第6版 中文版 清晰有書簽PDF+源代碼 http://www.linuxidc.com/Linux/2014-05/101227.htm

Copyright © Linux教程網 All Rights Reserved