網站首頁 編程語言 正文
前言
早期在學習泛型的協變與逆變時,網上的文章講解、例子算是能看懂,但關于逆變的具體應用場景這方面的知識,我并沒有深刻的認識。
本文將在具體的場景下,從泛型接口設計的角度出發,逐步探討逆變的作用,以及它能幫助我們解決哪方面的問題?
這篇文章算是協變、逆變知識的感悟和分享,開始之前,你應該先了解協變、逆變的基本概念,以及依賴注入,這類文章很多,這里就不再贅述。
協變的應用場景
雖然協變不是今天的主要內容,但在此之前,我還是想提一下關于協變的應用場景。
其中最常見的應用場景就是——如果方法的某個參數是一個集合時,我習慣將這個集合參數定義為IEnumerable<T>
類型。
class Program { public static void Save(IEnumerable<Animal> animals) { // TODO } } public class Animal { }
IEnumerable<T>
中的T
就是標記了代表協變的關鍵字out
namespace System.Collections.Generic { public interface IEnumerable<out T> : IEnumerable { IEnumerator<T> GetEnumerator(); } }
假如泛型T
為父類Animal
類型,Dog
為Animal
的子類,其他人在調用這個方法時,
不僅可以傳入IEnumerable<Animal>
、List<Animal>
、Animal[]
類型的參數,
還可以傳入IEnumerable<Dog>
、List<Dog>
、Dog[]
等其他繼承自IEnumerable<Animal>
類型的參數。
這樣,方法的兼容性會更強。
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 { }
逆變的應用場景
提起逆變,可能大家見過類似下面這段代碼:
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> { // 省略具體實現 }
IComparer<T>
中的T
就是標記了代表逆變的關鍵字in
namespace System.Collections.Generic { public interface IComparer<in T> { int Compare(T? x, T? y); } }
在看完這段代碼后,不知道你們是否跟我有一樣的想法:道理都懂,可是具體的應用場景呢?
要探索逆變可以幫助我們解決哪些問題,我們試著從另一個角度出發——在某個場景下,不使用逆變,是否會遇到某些問題。
假設我們需要保存各種基礎資料,根據需求我們定義了對應的接口,以及完成了對應接口的實現。這里假設Animal
與Human
就是其中的兩種基礎資料類型。
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 { }
現在增加一個批量保存基礎資料的功能,并且實時返回保存進度。
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); // 省略監聽進度代碼 } } public void BatchSaveHuman(IEnumerable<Human> entities) { foreach (var human in entities) { _humanSvc.Save(human); // 省略監聽進度代碼 } } }
完成上面代碼后,我們可以發現,監聽進度的代碼寫了兩次,如果像這樣的基礎資料類型很多,想要修改監聽進度的代碼,則會牽一發而動全身,這樣的代碼就不便于維護。
為了使代碼能夠復用,我們需要抽象出一個保存基礎資料的接口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()
合并為一個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()會在無對應接口實現時拋出錯誤 foreach (T entity in entities) { service.Save(entity); // 省略監聽進度代碼 } } }
重構后的代碼達到了可復用、易維護的目的,但很快你會發現新的問題。
在調用重構后的BatchSave<T>()
時,傳入Human
類型的集合參數,或Animal
類型的集合參數,代碼能夠正常運行,但在傳入Dog
類型的集合參數時,代碼運行到第8行就會報錯,因為我們并沒有實現ISave<Dog>
接口。
雖然Dog
是Animal
的子類,但卻不能使用保存Animal
的方法,這肯定會被接口調用者吐槽,因為它不符合里氏替換原則。
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);// 由于沒有實現ISave<Dog>接口,因此代碼運行時會報錯 }
在T
為Dog
時,要想獲取ISave<Animal>
這個不相關的服務,我們可以從IServiceCollection
服務集合中去找。
雖然我們拿到了注冊的所有服務,但如何才能在T
為Dog
類型時,拿到對應的ISave<Animal>
服務呢?
這時,逆變就派上用場了,我們將接口ISave<T>
加上關鍵字in
后,就可以將ISave<Animal>
分配給ISave<Dog>
public interface ISave<in T>// 加上關鍵字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) { // 假設T為Dog,只有在ISave<T>接口標記為逆變時, // typeof(ISave<Animal>).IsAssignableTo(typeof(ISave<Dog>)),才會是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); // 省略監聽進度代碼 } } }
現在BatchSave<T>()
算是符合里氏替換原則,但這樣的寫法也有缺點
優點:調用時,寫法干凈簡潔,不需要設置過多的泛型參數,只需要傳入對應的參數變量即可。
缺點:如果傳入的參數沒有對應的接口實現,編譯仍然會通過,只有在代碼運行時才會報錯,提示不夠積極、友好。
并且如果我們實現了ISave<Dog>
接口,那代碼運行到第16行時會得到ISave<Dog>
和ISave<Animal>
兩個結果,不具有唯一性。
要想在錯誤使用接口時,編譯器及時提示錯誤,可以將接口重構成下面這樣
public class BatchSaveService { private static readonly IServiceProvider _svcProvider; // 省略依賴注入代碼 // 增加一個泛型參數TService,用來指定調用哪個服務的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); // 省略監聽進度代碼 } } } 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); // 假如實現了繼承ISave<Dog>的接口IDogService,可以改為 // saveSvc.BatchSave<IDogService, Dog>(dogs); } }
這樣在錯誤使用接口時,編譯器就會及時報錯,但由于需要設置多個泛型參數,使用起來會有些麻煩。
關于?C# 協變和逆變 msdn?解釋如下:?
“協變”是指能夠使用與原始指定的派生類型相比,派生程度更大的類型。?
“逆變”則是指能夠使用派生程度更小的類型。?
解釋的很正確,大致就是這樣,不過不夠直白。?
直白的理解:?
“協變”->”和諧的變”->”很自然的變化”->string->object :協變。?
“逆變”->”逆常的變”->”不正常的變化”->object->string?逆變。?
上面是個人對協變和逆變的理解,比起記住那些派生,類型,原始指定,更大,更小之類的詞語,個人認為要容易點。?
討論
以上是我遇見的比較常見的關于逆變的應用場景,上述兩種方式你覺得哪種更好?是否有更好的設計方式?或者大家在寫代碼時遇見過哪些逆變的應用場景?
總結
原文鏈接:https://www.cnblogs.com/tinymad/p/15785546.html
相關推薦
- 2022-01-17 MongoDB啟動失敗: 此應用無法在你的電腦上運行
- 2022-09-18 Redis6?主從復制及哨兵機制的實現_Redis
- 2023-04-01 Python使用pptx實現復制頁面到其他PPT中_python
- 2022-06-02 Pygame實現簡易版趣味小游戲之反彈球_python
- 2022-07-20 C語言詳細實現猜拳游戲流程_C 語言
- 2022-02-20 Android?WebView實現全屏播放視頻_Android
- 2022-07-26 Nginx本地配置SSL訪問的實例教程_nginx
- 2024-04-04 netty使用http和webSocket
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支