C#中逆变的实际应用场景详解_第1页
C#中逆变的实际应用场景详解_第2页
C#中逆变的实际应用场景详解_第3页
C#中逆变的实际应用场景详解_第4页
C#中逆变的实际应用场景详解_第5页
已阅读5页,还剩4页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

第C#中逆变的实际应用场景详解目录前言协变的应用场景逆变的应用场景讨论总结

前言

早期在学习泛型的协变与逆变时,网上的文章讲解、例子算是能看懂,但关于逆变的具体应用场景这方面的知识,我并没有深刻的认识。

本文将在具体的场景下,从泛型接口设计的角度出发,逐步探讨逆变的作用,以及它能帮助我们解决哪方面的问题?

这篇文章算是协变、逆变知识的感悟和分享,开始之前,你应该先了解协变、逆变的基本概念,以及依赖注入,这类文章很多,这里就不再赘述。

协变的应用场景

虽然协变不是今天的主要内容,但在此之前,我还是想提一下关于协变的应用场景。

其中最常见的应用场景就是如果方法的某个参数是一个集合时,我习惯将这个集合参数定义为IEnumerableT类型。

classProgram

publicstaticvoidSave(IEnumerableAnimalanimals)

//TODO

publicclassAnimal{}

IEnumerableT中的T就是标记了代表协变的关键字out

namespaceSystem.Collections.Generic

publicinterfaceIEnumerableoutT:IEnumerable

IEnumeratorTGetEnumerator();

}

假如泛型T为父类Animal类型,Dog为Animal的子类,其他人在调用这个方法时,

不仅可以传入IEnumerableAnimal、ListAnimal、Animal[]类型的参数,

还可以传入IEnumerableDog、ListDog、Dog[]等其他继承自IEnumerableAnimal类型的参数。

这样,方法的兼容性会更强。

classProgram

publicstaticvoidSave(IEnumerableAnimalanimals)

//TODO

staticvoidMain(string[]args)

varanimalList=newListAnimal

varanimalArray=newAnimal[]{};

vardogList=newListDog

vardogArray=newDog[]{};

Save(animalList);

Save(animalArray);

Save(dogList);

Save(dogArray);

publicclassAnimal{}

publicclassDog:Animal{}

逆变的应用场景

提起逆变,可能大家见过类似下面这段代码:

classProgram

staticvoidMain(string[]args)

IComparerAnimalanimalComparer=newAnimalComparer();

IComparerDogdogComparer=animalComparer;//将IComparerAnimal赋值给IComparerDog

publicclassAnimalComparer:IComparerAnimal

//省略具体实现

}

IComparerT中的T就是标记了代表逆变的关键字in

namespaceSystem.Collections.Generic

publicinterfaceIComparerinT

intCompare(Tx,Ty);

}

在看完这段代码后,不知道你们是否跟我有一样的想法:道理都懂,可是具体的应用场景呢?

要探索逆变可以帮助我们解决哪些问题,我们试着从另一个角度出发在某个场景下,不使用逆变,是否会遇到某些问题。

假设我们需要保存各种基础资料,根据需求我们定义了对应的接口,以及完成了对应接口的实现。这里假设Animal与Human就是其中的两种基础资料类型。

publicinterfaceIAnimalService

voidSave(Animalentity);

publicinterfaceIHumanService

voidSave(Humanentity);

publicclassAnimalService:IAnimalService

publicvoidSave(Animalentity)

//TODO

publicclassHumanService:IHumanService

publicvoidSave(Humanentity)

//TODO

publicclassAnimal{}

publicclassHuman{}

现在增加一个批量保存基础资料的功能,并且实时返回保存进度。

publicclassBatchSaveService

privatestaticreadonlyIAnimalService_animalSvc;

privatestaticreadonlyIHumanService_humanSvc;

//省略依赖注入代码

publicvoidBatchSaveAnimal(IEnumerableAnimalentities)

foreach(varanimalinentities)

_animalSvc.Save(animal);

//省略监听进度代码

publicvoidBatchSaveHuman(IEnumerableHumanentities)

foreach(varhumaninentities)

_humanSvc.Save(human);

//省略监听进度代码

}

完成上面代码后,我们可以发现,监听进度的代码写了两次,如果像这样的基础资料类型很多,想要修改监听进度的代码,则会牵一发而动全身,这样的代码就不便于维护。

为了使代码能够复用,我们需要抽象出一个保存基础资料的接口ISaveT。

使IAnimalService、IHumanService继承ISaveT,将泛型T分别定义为Animal、Human

publicinterfaceISaveT

voidSave(Tentity);

publicinterfaceIAnimalService:ISaveAnimal{}

publicinterfaceIHumanService:ISaveHuman{}

这样,就可以将BatchSaveAnimal()和BatchSaveHuman()合并为一个BatchSaveT()

publicclassBatchSaveService

privatestaticreadonlyIServiceProvider_svcProvider;

//省略依赖注入代码

publicvoidBatchSaveT(IEnumerableTentities)

ISaveTservice=_svcProvider.GetRequiredServiceISaveT();//GetRequiredService()会在无对应接口实现时抛出错误

foreach(Tentityinentities)

service.Save(entity);

//省略监听进度代码

}

重构后的代码达到了可复用、易维护的目的,但很快你会发现新的问题。

在调用重构后的BatchSaveT()时,传入Human类型的集合参数,或Animal类型的集合参数,代码能够正常运行,但在传入Dog类型的集合参数时,代码运行到第8行就会报错,因为我们并没有实现ISaveDog接口。

虽然Dog是Animal的子类,但却不能使用保存Animal的方法,这肯定会被接口调用者吐槽,因为它不符合里氏替换原则。

staticvoidMain(string[]args)

ListHumanhumans=new(){newHuman()};

ListAnimalanimals=new(){newAnimal()};

ListDogdogs=new(){newDog()};

varsaveSvc=newBatchSaveService();

saveSvc.BatchSave(humans);

saveSvc.BatchSave(animals);

saveSvc.BatchSave(dogs);//由于没有实现ISaveDog接口,因此代码运行时会报错

}

在T为Dog时,要想获取ISaveAnimal这个不相关的服务,我们可以从IServiceCollection服务集合中去找。

虽然我们拿到了注册的所有服务,但如何才能在T为Dog类型时,拿到对应的ISaveAnimal服务呢?

这时,逆变就派上用场了,我们将接口ISaveT加上关键字in后,就可以将ISaveAnimal分配给ISaveDog

publicinterfaceISaveinT//加上关键字in

voidSave(Tentity);

publicclassBatchSaveService

privatestaticreadonlyIServiceProvider_svcProvider;

privatestaticreadonlyIServiceCollection_svcCollection;

//省略依赖注入代码

publicvoidBatchSaveT(IEnumerableTentities)

//假设T为Dog,只有在ISaveT接口标记为逆变时,

//typeof(ISaveAnimal).IsAssignableTo(typeof(ISaveDog)),才会是true

TypeserviceType=_svcCollection.Single(x=x.ServiceType.IsAssignableTo(typeof(ISaveT))).ServiceType;

ISaveTservice=_svcProvider.GetRequiredService(serviceType)asISaveT//ISaveAnimalasISaveDog

foreach(Tentityinentities)

service.Save(entity);

//省略监听进度代码

}

现在BatchSaveT()算是符合里氏替换原则,但这样的写法也有缺点

优点:调用时,写法干净简洁,不需要设置过多的泛型参数,只需要传入对应的参数变量即可。

缺点:如果传入的参数没有对应的接口实现,编译仍然会通过,只有在代码运行时才会报错,提示不够积极、友好。

并且如果我们实现了ISaveDog接口,那代码运行到第16行时会得到ISaveDog和ISaveAnimal两个结果,不具有唯一性。

要想在错误使用接口时,编译器及时提示错误,可以将接口重构成下面这样

publicclassBatchSaveService

privatestaticreadonlyIServiceProvider_svcProvider;

//省略依赖注入代码

//增加一个泛型参数TService,用来指定调用哪个服务的Save()

//并约定TService:ISaveT

publicvoidBatchSaveTService,T(IEnumerableTentities)whereTService:ISaveT

ISaveTservice=_svcProvider.GetServiceTService

foreach(Tentityinentities)

service.Save(entity);

//省略监听进度代码

classProgram

staticvoidMain(string[]args)

ListHumanhumans=new(){newHuman()};

ListAnimalanimals=new(){newAnimal()};

ListDogdogs=new(){newDog()};

varsaveSvc=newBatchSaveService();

saveSvc.BatchSaveIHumanServ

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论