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é)變、逆變的基本概念,以及依賴注入,這類文章很多,這里就不再贅述。
協(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>類型。
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為父類Animal類型,Dog為Animal的子類,其他人在調(diào)用這個(gè)方法時(shí),
不僅可以傳入IEnumerable<Animal>、List<Animal>、Animal[]類型的參數(shù),
還可以傳入IEnumerable<Dog>、List<Dog>、Dog[]等其他繼承自IEnumerable<Animal>類型的參數(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ò)類似下面這段代碼:
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ǔ)資料類型。
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;
// 省略依賴注入代碼
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)度的代碼寫了兩次,如果像這樣的基礎(chǔ)資料類型很多,想要修改監(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;
// 省略依賴注入代碼
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類型的集合參數(shù),或Animal類型的集合參數(shù),代碼能夠正常運(yùn)行,但在傳入Dog類型的集合參數(shù)時(shí),代碼運(yùn)行到第8行就會(huì)報(bào)錯(cuò),因?yàn)槲覀儾](méi)有實(shí)現(xiàn)ISave<Dog>接口。
雖然Dog是Animal的子類,但卻不能使用保存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類型時(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;
// 省略依賴注入代碼
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>()算是符合里氏替換原則,但這樣的寫法也有缺點(diǎn)
優(yōu)點(diǎn):調(diào)用時(shí),寫法干凈簡(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;
// 省略依賴注入代碼
// 增加一個(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é)變”是指能夠使用與原始指定的派生類型相比,派生程度更大的類型。
“逆變”則是指能夠使用派生程度更小的類型。
解釋的很正確,大致就是這樣,不過(guò)不夠直白。
直白的理解:
“協(xié)變”->”和諧的變”->”很自然的變化”->string->object :協(xié)變。
“逆變”->”逆常的變”->”不正常的變化”->object->string 逆變。
上面是個(gè)人對(duì)協(xié)變和逆變的理解,比起記住那些派生,類型,原始指定,更大,更小之類的詞語(yǔ),個(gè)人認(rèn)為要容易點(diǎn)。
討論
以上是我遇見(jiàn)的比較常見(jiàn)的關(guān)于逆變的應(yīng)用場(chǎng)景,上述兩種方式你覺(jué)得哪種更好?是否有更好的設(shè)計(jì)方式?或者大家在寫代碼時(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-08
C#使用foreach遍歷哈希表(hashtable)的方法
這篇文章主要介紹了C#使用foreach遍歷哈希表(hashtable)的方法,是C#中foreach語(yǔ)句遍歷散列表的典型應(yīng)用,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2015-04-04
C#基于WebSocket實(shí)現(xiàn)聊天室功能
這篇文章主要為大家詳細(xì)介紹了C#基于WebSocket實(shí)現(xiàn)聊天室功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-02-02

