C#中逆變的實(shí)際應(yīng)用場(chǎng)景詳解
前言
早期在學(xué)習(xí)泛型的協(xié)變與逆變時(shí),網(wǎng)上的文章講解、例子算是能看懂,但關(guān)于逆變的具體應(yīng)用場(chǎng)景這方面的知識(shí),我并沒(méi)有深刻的認(rèn)識(shí)。
本文將在具體的場(chǎng)景下,從泛型接口設(shè)計(jì)的角度出發(fā),逐步探討逆變的作用,以及它能幫助我們解決哪方面的問(wèn)題?
這篇文章算是協(xié)變、逆變知識(shí)的感悟和分享,開(kāi)始之前,你應(yīng)該先了解協(xié)變、逆變的基本概念,以及依賴(lài)注入,這類(lèi)文章很多,這里就不再贅述。
協(xié)變的應(yīng)用場(chǎng)景
雖然協(xié)變不是今天的主要內(nèi)容,但在此之前,我還是想提一下關(guān)于協(xié)變的應(yīng)用場(chǎng)景。
其中最常見(jiàn)的應(yīng)用場(chǎng)景就是——如果方法的某個(gè)參數(shù)是一個(gè)集合時(shí),我習(xí)慣將這個(gè)集合參數(shù)定義為IEnumerable<T>
類(lèi)型。
class Program { public static void Save(IEnumerable<Animal> animals) { // TODO } } public class Animal { }
IEnumerable<T>
中的T
就是標(biāo)記了代表協(xié)變的關(guān)鍵字out
namespace System.Collections.Generic { public interface IEnumerable<out T> : IEnumerable { IEnumerator<T> GetEnumerator(); } }
假如泛型T
為父類(lèi)Animal
類(lèi)型,Dog
為Animal
的子類(lèi),其他人在調(diào)用這個(gè)方法時(shí),
不僅可以傳入IEnumerable<Animal>
、List<Animal>
、Animal[]
類(lèi)型的參數(shù),
還可以傳入IEnumerable<Dog>
、List<Dog>
、Dog[]
等其他繼承自IEnumerable<Animal>
類(lèi)型的參數(shù)。
這樣,方法的兼容性會(huì)更強(qiáng)。
class Program { public static void Save(IEnumerable<Animal> animals) { // TODO } static void Main(string[] args) { var animalList = new List<Animal>(); var animalArray = new Animal[] { }; var dogList = new List<Dog>(); var dogArray = new Dog[] { }; Save(animalList); Save(animalArray); Save(dogList); Save(dogArray); } } public class Animal { } public class Dog : Animal { }
逆變的應(yīng)用場(chǎng)景
提起逆變,可能大家見(jiàn)過(guò)類(lèi)似下面這段代碼:
class Program { static void Main(string[] args) { IComparer<Animal> animalComparer = new AnimalComparer(); IComparer<Dog> dogComparer = animalComparer;// 將 IComparer<Animal> 賦值給 IComparer<Dog> } } public class AnimalComparer : IComparer<Animal> { // 省略具體實(shí)現(xiàn) }
IComparer<T>
中的T
就是標(biāo)記了代表逆變的關(guān)鍵字in
namespace System.Collections.Generic { public interface IComparer<in T> { int Compare(T? x, T? y); } }
在看完這段代碼后,不知道你們是否跟我有一樣的想法:道理都懂,可是具體的應(yīng)用場(chǎng)景呢?
要探索逆變可以幫助我們解決哪些問(wèn)題,我們?cè)囍鴱牧硪粋€(gè)角度出發(fā)——在某個(gè)場(chǎng)景下,不使用逆變,是否會(huì)遇到某些問(wèn)題。
假設(shè)我們需要保存各種基礎(chǔ)資料,根據(jù)需求我們定義了對(duì)應(yīng)的接口,以及完成了對(duì)應(yīng)接口的實(shí)現(xiàn)。這里假設(shè)Animal
與Human
就是其中的兩種基礎(chǔ)資料類(lèi)型。
public interface IAnimalService { void Save(Animal entity); } public interface IHumanService { void Save(Human entity); } public class AnimalService : IAnimalService { public void Save(Animal entity) { // TODO } } public class HumanService : IHumanService { public void Save(Human entity) { // TODO } } public class Animal { } public class Human { }
現(xiàn)在增加一個(gè)批量保存基礎(chǔ)資料的功能,并且實(shí)時(shí)返回保存進(jìn)度。
public class BatchSaveService { private static readonly IAnimalService _animalSvc; private static readonly IHumanService _humanSvc; // 省略依賴(lài)注入代碼 public void BatchSaveAnimal(IEnumerable<Animal> entities) { foreach (var animal in entities) { _animalSvc.Save(animal); // 省略監(jiān)聽(tīng)進(jìn)度代碼 } } public void BatchSaveHuman(IEnumerable<Human> entities) { foreach (var human in entities) { _humanSvc.Save(human); // 省略監(jiān)聽(tīng)進(jìn)度代碼 } } }
完成上面代碼后,我們可以發(fā)現(xiàn),監(jiān)聽(tīng)進(jìn)度的代碼寫(xiě)了兩次,如果像這樣的基礎(chǔ)資料類(lèi)型很多,想要修改監(jiān)聽(tīng)進(jìn)度的代碼,則會(huì)牽一發(fā)而動(dòng)全身,這樣的代碼就不便于維護(hù)。
為了使代碼能夠復(fù)用,我們需要抽象出一個(gè)保存基礎(chǔ)資料的接口ISave<T>
。
使IAnimalService
、IHumanService
繼承ISave<T>
,將泛型T
分別定義為Animal
、Human
public interface ISave<T> { void Save(T entity); } public interface IAnimalService : ISave<Animal> { } public interface IHumanService : ISave<Human> { }
這樣,就可以將BatchSaveAnimal()
和BatchSaveHuman()
合并為一個(gè)BatchSave<T>()
public class BatchSaveService { private static readonly IServiceProvider _svcProvider; // 省略依賴(lài)注入代碼 public void BatchSave<T>(IEnumerable<T> entities) { ISave<T> service = _svcProvider.GetRequiredService<ISave<T>>();// GetRequiredService()會(huì)在無(wú)對(duì)應(yīng)接口實(shí)現(xiàn)時(shí)拋出錯(cuò)誤 foreach (T entity in entities) { service.Save(entity); // 省略監(jiān)聽(tīng)進(jìn)度代碼 } } }
重構(gòu)后的代碼達(dá)到了可復(fù)用、易維護(hù)的目的,但很快你會(huì)發(fā)現(xiàn)新的問(wèn)題。
在調(diào)用重構(gòu)后的BatchSave<T>()
時(shí),傳入Human
類(lèi)型的集合參數(shù),或Animal
類(lèi)型的集合參數(shù),代碼能夠正常運(yùn)行,但在傳入Dog
類(lèi)型的集合參數(shù)時(shí),代碼運(yùn)行到第8行就會(huì)報(bào)錯(cuò),因?yàn)槲覀儾](méi)有實(shí)現(xiàn)ISave<Dog>
接口。
雖然Dog
是Animal
的子類(lèi),但卻不能使用保存Animal
的方法,這肯定會(huì)被接口調(diào)用者吐槽,因?yàn)樗环?strong>里氏替換原則。
static void Main(string[] args) { List<Human> humans = new() { new Human() }; List<Animal> animals = new() { new Animal() }; List<Dog> dogs = new() { new Dog() }; var saveSvc = new BatchSaveService(); saveSvc.BatchSave(humans); saveSvc.BatchSave(animals); saveSvc.BatchSave(dogs);// 由于沒(méi)有實(shí)現(xiàn)ISave<Dog>接口,因此代碼運(yùn)行時(shí)會(huì)報(bào)錯(cuò) }
在T
為Dog
時(shí),要想獲取ISave<Animal>
這個(gè)不相關(guān)的服務(wù),我們可以從IServiceCollection
服務(wù)集合中去找。
雖然我們拿到了注冊(cè)的所有服務(wù),但如何才能在T
為Dog
類(lèi)型時(shí),拿到對(duì)應(yīng)的ISave<Animal>
服務(wù)呢?
這時(shí),逆變就派上用場(chǎng)了,我們將接口ISave<T>
加上關(guān)鍵字in
后,就可以將ISave<Animal>
分配給ISave<Dog>
public interface ISave<in T>// 加上關(guān)鍵字in { void Save(T entity); } public class BatchSaveService { private static readonly IServiceProvider _svcProvider; private static readonly IServiceCollection _svcCollection; // 省略依賴(lài)注入代碼 public void BatchSave<T>(IEnumerable<T> entities) { // 假設(shè)T為Dog,只有在ISave<T>接口標(biāo)記為逆變時(shí), // typeof(ISave<Animal>).IsAssignableTo(typeof(ISave<Dog>)),才會(huì)是true Type serviceType = _svcCollection.Single(x => x.ServiceType.IsAssignableTo(typeof(ISave<T>))).ServiceType; ISave<T> service = _svcProvider.GetRequiredService(serviceType) as ISave<T>;// ISave<Animal> as ISave<Dog> foreach (T entity in entities) { service.Save(entity); // 省略監(jiān)聽(tīng)進(jìn)度代碼 } } }
現(xiàn)在BatchSave<T>()
算是符合里氏替換原則,但這樣的寫(xiě)法也有缺點(diǎn)
優(yōu)點(diǎn):調(diào)用時(shí),寫(xiě)法干凈簡(jiǎn)潔,不需要設(shè)置過(guò)多的泛型參數(shù),只需要傳入對(duì)應(yīng)的參數(shù)變量即可。
缺點(diǎn):如果傳入的參數(shù)沒(méi)有對(duì)應(yīng)的接口實(shí)現(xiàn),編譯仍然會(huì)通過(guò),只有在代碼運(yùn)行時(shí)才會(huì)報(bào)錯(cuò),提示不夠積極、友好。
并且如果我們實(shí)現(xiàn)了ISave<Dog>
接口,那代碼運(yùn)行到第16行時(shí)會(huì)得到ISave<Dog>
和ISave<Animal>
兩個(gè)結(jié)果,不具有唯一性。
要想在錯(cuò)誤使用接口時(shí),編譯器及時(shí)提示錯(cuò)誤,可以將接口重構(gòu)成下面這樣
public class BatchSaveService { private static readonly IServiceProvider _svcProvider; // 省略依賴(lài)注入代碼 // 增加一個(gè)泛型參數(shù)TService,用來(lái)指定調(diào)用哪個(gè)服務(wù)的Save() // 并約定 TService : ISave<T> public void BatchSave<TService, T>(IEnumerable<T> entities) where TService : ISave<T> { ISave<T> service = _svcProvider.GetService<TService>(); foreach (T entity in entities) { service.Save(entity); // 省略監(jiān)聽(tīng)進(jìn)度代碼 } } } class Program { static void Main(string[] args) { List<Human> humans = new() { new Human() }; List<Animal> animals = new() { new Animal() }; List<Dog> dogs = new() { new Dog() }; var saveSvc = new BatchSaveService(); saveSvc.BatchSave<IHumanService, Human>(humans); saveSvc.BatchSave<IAnimalService, Animal>(animals); saveSvc.BatchSave<IAnimalService, Dog>(dogs); // 假如實(shí)現(xiàn)了繼承ISave<Dog>的接口IDogService,可以改為 // saveSvc.BatchSave<IDogService, Dog>(dogs); } }
這樣在錯(cuò)誤使用接口時(shí),編譯器就會(huì)及時(shí)報(bào)錯(cuò),但由于需要設(shè)置多個(gè)泛型參數(shù),使用起來(lái)會(huì)有些麻煩。
關(guān)于 C# 協(xié)變和逆變 msdn 解釋如下:
“協(xié)變”是指能夠使用與原始指定的派生類(lèi)型相比,派生程度更大的類(lèi)型。
“逆變”則是指能夠使用派生程度更小的類(lèi)型。
解釋的很正確,大致就是這樣,不過(guò)不夠直白。
直白的理解:
“協(xié)變”->”和諧的變”->”很自然的變化”->string->object :協(xié)變。
“逆變”->”逆常的變”->”不正常的變化”->object->string 逆變。
上面是個(gè)人對(duì)協(xié)變和逆變的理解,比起記住那些派生,類(lèi)型,原始指定,更大,更小之類(lèi)的詞語(yǔ),個(gè)人認(rèn)為要容易點(diǎn)。
討論
以上是我遇見(jiàn)的比較常見(jiàn)的關(guān)于逆變的應(yīng)用場(chǎng)景,上述兩種方式你覺(jué)得哪種更好?是否有更好的設(shè)計(jì)方式?或者大家在寫(xiě)代碼時(shí)遇見(jiàn)過(guò)哪些逆變的應(yīng)用場(chǎng)景?
總結(jié)
到此這篇關(guān)于C#中逆變實(shí)際應(yīng)用場(chǎng)景的文章就介紹到這了,更多相關(guān)C# 逆變應(yīng)用場(chǎng)景內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
總結(jié)C#刪除字符串?dāng)?shù)組中空字符串的幾種方法
C#中要如何才能刪除一個(gè)字符串?dāng)?shù)組中的空字符串呢?下面的文章會(huì)介紹多種方式來(lái)實(shí)現(xiàn)清除數(shù)組中的空字符串,以及在.net中將字符串?dāng)?shù)組中字符串為空的元素去除。2016-08-08C#使用foreach遍歷哈希表(hashtable)的方法
這篇文章主要介紹了C#使用foreach遍歷哈希表(hashtable)的方法,是C#中foreach語(yǔ)句遍歷散列表的典型應(yīng)用,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2015-04-04C# WCF簡(jiǎn)單入門(mén)圖文教程(VS2010版)
這篇文章主要介紹了WCF簡(jiǎn)單入門(mén)圖文教程,版本是VS2010版,幫助大家輕松學(xué)習(xí)了解DataContract、ServiceContract等特性,感興趣的小伙伴們可以參考一下2016-03-03c# 修改windows中賬戶(hù)的用戶(hù)名和密碼
這篇文章主要介紹了c# 改變windows中賬戶(hù)的用戶(hù)名和密碼,幫助大家更好的理解和學(xué)習(xí)C#,感興趣的朋友可以了解下2020-11-11C#基于WebSocket實(shí)現(xiàn)聊天室功能
這篇文章主要為大家詳細(xì)介紹了C#基于WebSocket實(shí)現(xiàn)聊天室功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-02-02