泛型的可變性:協變性和逆變性
實質上,可變性是以一種類型安全的方式,將一個對象作爲另一個對象來使用。
我們已經習慣了普通繼承中的可變性:例如,若某方法聲明返回類型爲Stream,在實現時可以返回一個MemoryStream。泛型可變性的概念與此相同,但要略微複雜一些。可變性應用於泛型接口和泛型委託的類型參數中,這一點必須引起注意。
可變性有兩種類型:協變性和逆變性。二者概念基本相同,只是在上下文中轉換的方向不同。
我們先從協變性開始,它通常要好理解一些。
-
協變性:從API返回的值
協變性用於向調用者返回某項操作的值。例如一個簡單的表示工廠模式的泛型接口,它只包含一個方法CreateInstanse,返回適當類型的實例。代碼如下:
/// <summary> /// 使用out關鍵字表示協變 /// </summary> /// <typeparam name="T"></typeparam> interface IFactory<out T> { T CreateInstance(); }
現在,T在接口中只出現了一次(除了在簽名中),它僅作爲返回值使用,即方法的輸出。這意味着可以將特定類型的工廠視爲更一般類型的工廠。如在現實世界裏,你可以將比薩工廠視爲食品工廠。
-
逆變性:傳入API的值
逆變性則相反。它指的是調用者向API傳入值,即API是在消費值,而不是產生值。我們來想象另一個簡單的接口——它可以向控制檯打印特定的文檔類型。同樣,它也只有一個方法Print:
/// <summary> /// 使用in關鍵字表示逆變 /// </summary> /// <typeparam name="T"></typeparam> interface IPrettyPrinter<in T> { void Print(T document); }
這次T只作爲參數出現在了接口的輸入位置。具體而言,如果我們實現了IPrettyPrinter<SourceCode>,就可以將其當作IPrettyPrinter<CSharpCode>來使用。
不變性:雙向傳遞的值
如果協變性適用於僅從API輸出值的情況,而逆變性用於僅向API輸入值的情況,那麼如果值雙向傳遞會如何呢?簡而言之,什麼也不會發生。這種類型是不變體(invariant)。下面的接口表示可以對數據類型進行序列化和反序列化的類型:
/// <summary> /// 泛型類型的不變性,既不用 in 關鍵字限制,也不用 out 關鍵字限制 /// </summary> /// <typeparam name="T"></typeparam> interface IStorage<T> { byte[] Serialize(T value); T Deserialize(byte[] data); }
這時,如果存在一個具有特定類型T的IStorage<T>實例,我們不能將其視爲該接口更具體或更一般類型的實現。如果以協變的方式使用(如將IStorage<Customer>視爲IStorage<Person>),則可能在調用Serialize時傳入一個無法處理的對象。
類似地,如果以逆變的方式使用,則可能在反序列化數據時得到一個預料之外的類型。如果有助於理解的話,可以將不變性看成ref參數:按引用傳遞變量,其類型必須與參數本身的類型完全一致,因爲值被傳入了方法內部,並且同樣被高效地傳出。
更多
詳見MSDN:https://docs.microsoft.com/zh-cn/dotnet/standard/generics/covariance-and-contravariance