C#泛型的逆變協(xié)變之個人理解
一般來說, 泛型的作用就類似一個占位符, 或者說是一個參數(shù), 可以讓我們把類型像參數(shù)一樣進(jìn)行傳遞, 盡可能地復(fù)用代碼。
我有個朋友, 在使用的過程中發(fā)現(xiàn)一個問題
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>
實現(xiàn)的是 IFace<string>
, 而 IFace<string>
并不是 IFace<object>
的子類
Q: 但是 string
是 object
的子類啊, IFace<string>
可不就是 IFace<object>
嗎?
A: 如果只論接口定義, 看起來確實是這樣的, 但是你要看內(nèi)部實現(xiàn)的方法, IFace<string>
的 Print
方法參數(shù)是 string
, 但是 IFace<object>
的 Print
參數(shù)是 object
, 如果上面的賦值可以成立, 就意味著允許 Print(string input)
方法傳遞任意類型的對象, 這樣明顯是有問題的
Q: 但是我曾經(jīng)看到過 IEnumerable<object> list = new List<string>();
這個為什么就可以
A: 這就要講到C#泛型里的逆變協(xié)變了
Q: 細(xì)嗦細(xì)嗦
逆變協(xié)變
C#泛型中的逆變(in)協(xié)變(out)對于不常自定義泛型的開發(fā)來說(可能)是個很難理解的概念, 簡單來說其表現(xiàn)形式如下
逆變(in): I<子類> = I<父類>
協(xié)變(out): I<父類> = I<子類>
上面例子中提到的 IEnumerable<object> list = new List<string>();
體現(xiàn)的是協(xié)變
, 符合一般直覺, 整體上看起來就像是將子類賦值給基類
轉(zhuǎn)到 IEnumerable<>
的定義, 我們可以看到
public interface IEnumerable<out T> : IEnumerable { new IEnumerator<T> GetEnumerator(); }
泛型 T
之前加了協(xié)變的關(guān)鍵詞 out
, 代表支持協(xié)變, 可以進(jìn)行符合直覺且和諧的轉(zhuǎn)化
前編中提到的代碼例子不適用并且也不能改造成協(xié)變, 只適合使用逆變
相比于符合直覺且和諧的協(xié)變, 逆變是不符合直覺并且別扭的
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(); }
這是一個逆變的例子, 與協(xié)變相似, 需要在泛型 T
之前加上關(guān)鍵詞 in
對比上方的協(xié)變, 逆變看起來就像是將基類賦值給子類, 但這其實符合里氏代換的
當(dāng)我們調(diào)用 item.Print
時, 看起來允許傳入的參數(shù)為 string
類型, 而實際上最終調(diào)用的 Face<object>.Print
是支持 object
的, 傳入 string
類型的參數(shù)沒有任何問題
逆變協(xié)變的作用
逆變(in)協(xié)變(out)的作用就是擴(kuò)展泛型的用法, 幫助開發(fā)者更好地復(fù)用代碼, 同時通過約束限制可能會出現(xiàn)的破壞類型安全的操作
逆變協(xié)變的限制
雖然上面講了逆變(in)協(xié)變(out)看起來是什么樣的, 但我的那個朋友還是有些疑問
Q: 那我什么時候可以用逆變, 什么時候可以用協(xié)變, 這兩個東西用起來有什么限制?
A: 簡單來說, 有關(guān)泛型輸入
的用逆變
, 關(guān)鍵詞是in
, 有關(guān)泛型輸出
的用協(xié)變
, 關(guān)鍵詞是out
, 如果接口中既有輸入又有輸出, 就不能用逆變協(xié)變
Q: 為什么這兩個不能同時存在?
A: 協(xié)變
的表現(xiàn)形式為將子類賦值給基類
, 當(dāng)進(jìn)行輸出
相關(guān)操作時, 輸出的對象類型為基類, 是將子類轉(zhuǎn)為基類, 你可以說子類是基類;逆變
的表現(xiàn)形式為將基類賦值給子類
, 當(dāng)進(jìn)行輸入
相關(guān)操作時, 輸入的對象為子類, 是將子類轉(zhuǎn)為基類, 這個時候你也可以說基類是子類;
如果同時支持逆變協(xié)變, 若先進(jìn)行子類賦值給基類的操作, 此時輸出
的是基類, 子類轉(zhuǎn)為基類并不會有什么問題, 但進(jìn)行輸入
操作時就是在將基類轉(zhuǎn)為子類, 此時是無法保證類型安全的;
Q: 聽不懂, 能不能舉個例子給我?
A: 假設(shè) IEnumerable<>
同時支持逆變協(xié)變, IEnumerable<object> list = new List<string>();
進(jìn)行賦值后, list
中實際保存的類型是string
, item.First()
的輸出
類型為object
, 實際類型是string
, 此時說string
是object
沒有任何問題, 協(xié)變可以正常發(fā)揮作用;
但是如果支持了逆變, 假設(shè)我們進(jìn)行輸入
類型的操作, item.Add()
允許的參數(shù)類型為 object
, 可以是任意類型, 但是實際上支持string
類型, 此時的object
絕無可能是string
Q: 好像聽懂了一點了, 我以后慢慢琢磨吧
兩者的限制簡單總結(jié)就是
輸入
的用逆變
??????????????輸出
的用協(xié)變
相關(guān)文章
C#使用FluentHttpClient實現(xiàn)請求WebApi
FluentHttpClient 是一個REST API 異步調(diào)用 HTTP 客戶端,調(diào)用過程非常便捷,下面我們就來學(xué)習(xí)一下C#如何使用FluentHttpClient實現(xiàn)請求WebApi吧2023-12-12C#連接Oracle數(shù)據(jù)庫的多種方法總結(jié)
最近小項目當(dāng)中要使用C#來連接Oracle數(shù)據(jù)庫來完成系統(tǒng)的操作,這篇文章主要給大家介紹了關(guān)于C#連接Oracle數(shù)據(jù)庫的多種方法,文中通過實例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-04-04C#禁止textbox復(fù)制、粘貼、剪切及鼠標(biāo)右鍵的方法
這篇文章主要介紹了C#禁止textbox復(fù)制、粘貼、剪切及鼠標(biāo)右鍵的方法,涉及C#針對窗口消息的處理技巧,具有一定參考借鑒價值,需要的朋友可以參考下2015-09-09實現(xiàn)ASP.NET無刷新下載并提示下載完成的開發(fā)思路
這篇文章主要介紹了實現(xiàn)ASP.NET無刷新下載并提示下載完成的開發(fā)思路的相關(guān)資料,需要的朋友可以參考下2015-10-10C# .Net實現(xiàn)灰度圖和HeatMap熱力圖winform(進(jìn)階)
本文主要介紹了C# .NET實現(xiàn)簡易灰度圖和酷炫HeatMap熱力圖winform,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-12-12C#實現(xiàn)Post數(shù)據(jù)或文件到指定的服務(wù)器進(jìn)行接收
這篇文章主要為大家詳細(xì)介紹了如何通過C#實現(xiàn)Post數(shù)據(jù)或文件到指定的服務(wù)器進(jìn)行接收,文中的示例代碼講解詳細(xì),需要的小伙伴可以參考下2024-03-03