一般來說, 泛型的作用就類似一個預留位置, 或者說是一個引數, 可以讓我們把型別像引數一樣進行傳遞, 儘可能地複用程式碼
我有個朋友, 在使用的過程中發現一個問題
IFace<object> item = new Face<string>(); // CS0266
public interface IFace<T>
{
string Print(T input);
}
public class Face<T> : IFace<T>
{
public string Print(T input) => input.ToString();
}
Q: string
明明是 object
的子類, 為啥這樣賦值會報錯呢???
A: 因為 Face<string>
實現的是 IFace<string>
, 而 IFace<string>
並不是 IFace<object>
的子類
Q: 但是 string
是 object
的子類啊, IFace<string>
可不就是 IFace<object>
嗎?
A: 如果只論介面定義, 看起來確實是這樣的, 但是你要看內部實現的方法, IFace<string>
的 Print
方法引數是 string
, 但是 IFace<object>
的 Print
引數是 object
, 如果上面的賦值可以成立, 就意味著允許 Print(string input)
方法傳遞任意型別的物件, 這樣明顯是有問題的
Q: 但是我曾經看到過 IEnumerable<object> list = new List<string>();
這個為什麼就可以
A: 這就要講到C#泛型裡的逆變協變了
Q: 細嗦細嗦
C#泛型中的逆變(in)協變(out)對於不常自定義泛型的開發來說(可能)是個很難理解的概念, 簡單來說其表現形式如下
逆變(in): I<子類> = I<父類別>
協變(out): I<父類別> = I<子類>
上面例子中提到的 IEnumerable<object> list = new List<string>();
體現的是協變
, 符合一般直覺, 整體上看起來就像是將子類賦值給基礎類別
轉到 IEnumerable<>
的定義, 我們可以看到
public interface IEnumerable<out T> : IEnumerable
{
new IEnumerator<T> GetEnumerator();
}
泛型 T
之前加了協變的關鍵詞 out
, 代表支援協變, 可以進行符合直覺且和諧的轉化
前編中提到的程式碼例子不適用並且也不能改造成協變, 只適合使用逆變
相比於符合直覺且和諧的協變, 逆變是不符合直覺並且彆扭的
IFace<string> item = new Face<object>();
public interface IFace<in T>
{
string Print(T input);
}
public class Face<T> : IFace<T>
{
public string Print(T input) => input.ToString();
}
這是一個逆變的例子, 與協變相似, 需要在泛型 T
之前加上關鍵詞 in
對比上方的協變, 逆變看起來就像是將基礎類別賦值給子類, 但這其實符合里氏代換的
當我們呼叫 item.Print
時, 看起來允許傳入的引數為 string
型別, 而實際上最終呼叫的 Face<object>.Print
是支援 object
的, 傳入 string
型別的引數沒有任何問題
逆變(in)協變(out)的作用就是擴充套件泛型的用法, 幫助開發者更好地複用程式碼, 同時通過約束限制可能會出現的破壞型別安全的操作
雖然上面講了逆變(in)協變(out)看起來是什麼樣的, 但我的那個朋友還是有些疑問
Q: 那我什麼時候可以用逆變, 什麼時候可以用協變, 這兩個東西用起來有什麼限制?
A: 簡單來說, 有關泛型輸入
的用逆變
, 關鍵詞是in
, 有關泛型輸出
的用協變
, 關鍵詞是out
, 如果介面中既有輸入又有輸出, 就不能用逆變協變
Q: 為什麼這兩個不能同時存在?
A: 協變
的表現形式為將子類賦值給基礎類別
, 當進行輸出
相關操作時, 輸出的物件型別為基礎類別, 是將子類轉為基礎類別, 你可以說子類是基礎類別;
逆變
的表現形式為將基礎類別賦值給子類
, 當進行輸入
相關操作時, 輸入的物件為子類, 是將子類轉為基礎類別, 這個時候你也可以說基礎類別是子類;
如果同時支援逆變協變, 若先進行子類賦值給基礎類別的操作, 此時輸出
的是基礎類別, 子類轉為基礎類別並不會有什麼問題, 但進行輸入
操作時就是在將基礎類別轉為子類, 此時是無法保證型別安全的;
Q: 聽不懂, 能不能舉個例子給我?
A: 假設 IEnumerable<>
同時支援逆變協變, IEnumerable<object> list = new List<string>();
進行賦值後, list
中實際儲存的型別是string
, item.First()
的輸出
型別為object
, 實際型別是string
, 此時說string
是object
沒有任何問題, 協變可以正常發揮作用;
但是如果支援了逆變, 假設我們進行輸入
型別的操作, item.Add()
允許的引數型別為 object
, 可以是任意型別, 但是實際上支援string
型別, 此時的object
絕無可能是string
Q: 好像聽懂了一點了, 我以後慢慢琢磨吧
兩者的限制簡單總結就是
輸入
的用逆變
輸出
的用協變