亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

C#中逆變的實際應用場景詳解

 更新時間:2022年01月19日 10:35:36   作者:TinyMaD  
在好多的.net的書籍中都看到過逆變和協(xié)變的概念,也在網(wǎng)上搜了一些關于這兩個概念的解釋,但是一直感覺似懂非懂的,直到最近在項目中實際遇到了一個問題,恰好用到了逆變,下面這篇文章主要給大家介紹了關于C#中逆變的實際應用場景,需要的朋友可以參考下

前言

早期在學習泛型的協(xié)變與逆變時,網(wǎng)上的文章講解、例子算是能看懂,但關于逆變的具體應用場景這方面的知識,我并沒有深刻的認識。
本文將在具體的場景下,從泛型接口設計的角度出發(fā),逐步探討逆變的作用,以及它能幫助我們解決哪方面的問題?

這篇文章算是協(xié)變、逆變知識的感悟和分享,開始之前,你應該先了解協(xié)變、逆變的基本概念,以及依賴注入,這類文章很多,這里就不再贅述。

協(xié)變的應用場景

雖然協(xié)變不是今天的主要內(nèi)容,但在此之前,我還是想提一下關于協(xié)變的應用場景。

其中最常見的應用場景就是——如果方法的某個參數(shù)是一個集合時,我習慣將這個集合參數(shù)定義為IEnumerable<T>類型。

class Program
{
    public static void Save(IEnumerable<Animal> animals)
    {
        // TODO
    }
}
public class Animal { }

IEnumerable<T>中的T就是標記了代表協(xié)變的關鍵字out

namespace System.Collections.Generic
{
    public interface IEnumerable<out T> : IEnumerable
    {
        IEnumerator<T> GetEnumerator();
    }
}

假如泛型T為父類Animal類型,DogAnimal的子類,其他人在調(diào)用這個方法時,

不僅可以傳入IEnumerable<Animal>List<Animal>、Animal[]類型的參數(shù),

還可以傳入IEnumerable<Dog>、List<Dog>Dog[]等其他繼承自IEnumerable<Animal>類型的參數(shù)。

這樣,方法的兼容性會更強。

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>
{
    // 省略具體實現(xiàn)
}

IComparer<T>中的T就是標記了代表逆變的關鍵字in

namespace System.Collections.Generic
{
    public interface IComparer<in T>
    {
        int Compare(T? x, T? y);
    }
}

在看完這段代碼后,不知道你們是否跟我有一樣的想法:道理都懂,可是具體的應用場景呢?

要探索逆變可以幫助我們解決哪些問題,我們試著從另一個角度出發(fā)——在某個場景下,不使用逆變,是否會遇到某些問題。

假設我們需要保存各種基礎資料,根據(jù)需求我們定義了對應的接口,以及完成了對應接口的實現(xiàn)。這里假設AnimalHuman就是其中的兩種基礎資料類型。

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)在增加一個批量保存基礎資料的功能,并且實時返回保存進度。

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)聽進度代碼
        }
    }
    public void BatchSaveHuman(IEnumerable<Human> entities)
    {
        foreach (var human in entities)
        {
            _humanSvc.Save(human);
            // 省略監(jiān)聽進度代碼
        }
    }
}

完成上面代碼后,我們可以發(fā)現(xiàn),監(jiān)聽進度的代碼寫了兩次,如果像這樣的基礎資料類型很多,想要修改監(jiān)聽進度的代碼,則會牽一發(fā)而動全身,這樣的代碼就不便于維護。

為了使代碼能夠復用,我們需要抽象出一個保存基礎資料的接口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()會在無對應接口實現(xiàn)時拋出錯誤

        foreach (T entity in entities)
        {
            service.Save(entity);
            // 省略監(jiān)聽進度代碼
        }
    }
}

重構后的代碼達到了可復用、易維護的目的,但很快你會發(fā)現(xiàn)新的問題。

在調(diào)用重構后的BatchSave<T>()時,傳入Human類型的集合參數(shù),或Animal類型的集合參數(shù),代碼能夠正常運行,但在傳入Dog類型的集合參數(shù)時,代碼運行到第8行就會報錯,因為我們并沒有實現(xiàn)ISave<Dog>接口。

雖然DogAnimal的子類,但卻不能使用保存Animal的方法,這肯定會被接口調(diào)用者吐槽,因為它不符合里氏替換原則。

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);// 由于沒有實現(xiàn)ISave<Dog>接口,因此代碼運行時會報錯
}

TDog時,要想獲取ISave<Animal>這個不相關的服務,我們可以從IServiceCollection服務集合中去找。

雖然我們拿到了注冊的所有服務,但如何才能在TDog類型時,拿到對應的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);
            // 省略監(jiān)聽進度代碼
        }
    }
}

現(xiàn)在BatchSave<T>()算是符合里氏替換原則,但這樣的寫法也有缺點

  • 優(yōu)點:調(diào)用時,寫法干凈簡潔,不需要設置過多的泛型參數(shù),只需要傳入對應的參數(shù)變量即可。

  • 缺點:如果傳入的參數(shù)沒有對應的接口實現(xiàn),編譯仍然會通過,只有在代碼運行時才會報錯,提示不夠積極、友好。
    并且如果我們實現(xiàn)了ISave<Dog>接口,那代碼運行到第16行時會得到ISave<Dog>ISave<Animal>兩個結果,不具有唯一性。

要想在錯誤使用接口時,編譯器及時提示錯誤,可以將接口重構成下面這樣

public class BatchSaveService
{
    private static readonly IServiceProvider _svcProvider;
    // 省略依賴注入代碼

    // 增加一個泛型參數(shù)TService,用來指定調(diào)用哪個服務的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)聽進度代碼
        }
    }
}

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);
        // 假如實現(xiàn)了繼承ISave<Dog>的接口IDogService,可以改為
        // saveSvc.BatchSave<IDogService, Dog>(dogs);
    }
}

這樣在錯誤使用接口時,編譯器就會及時報錯,但由于需要設置多個泛型參數(shù),使用起來會有些麻煩。

關于 C# 協(xié)變和逆變 msdn 解釋如下: 

“協(xié)變”是指能夠使用與原始指定的派生類型相比,派生程度更大的類型。 

“逆變”則是指能夠使用派生程度更小的類型。 

解釋的很正確,大致就是這樣,不過不夠直白。 

直白的理解: 

“協(xié)變”->”和諧的變”->”很自然的變化”->string->object :協(xié)變。 

“逆變”->”逆常的變”->”不正常的變化”->object->string 逆變。 

上面是個人對協(xié)變和逆變的理解,比起記住那些派生,類型,原始指定,更大,更小之類的詞語,個人認為要容易點。 

討論

以上是我遇見的比較常見的關于逆變的應用場景,上述兩種方式你覺得哪種更好?是否有更好的設計方式?或者大家在寫代碼時遇見過哪些逆變的應用場景?

總結

到此這篇關于C#中逆變實際應用場景的文章就介紹到這了,更多相關C# 逆變應用場景內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

  • 總結C#刪除字符串數(shù)組中空字符串的幾種方法

    總結C#刪除字符串數(shù)組中空字符串的幾種方法

    C#中要如何才能刪除一個字符串數(shù)組中的空字符串呢?下面的文章會介紹多種方式來實現(xiàn)清除數(shù)組中的空字符串,以及在.net中將字符串數(shù)組中字符串為空的元素去除。
    2016-08-08
  • C#詞法分析器之構造NFA詳解

    C#詞法分析器之構造NFA詳解

    本篇文章介紹了,C#詞法分析器之構造NFA詳解。需要的朋友參考下
    2013-05-05
  • c# 使用OpenCV識別硬幣

    c# 使用OpenCV識別硬幣

    這篇文章主要介紹了c# 使用OpenCV識別硬幣的方法,幫助大家更好的利用c#進行深度學習,感興趣的朋友可以了解下
    2020-12-12
  • C#使用foreach遍歷哈希表(hashtable)的方法

    C#使用foreach遍歷哈希表(hashtable)的方法

    這篇文章主要介紹了C#使用foreach遍歷哈希表(hashtable)的方法,是C#中foreach語句遍歷散列表的典型應用,非常具有實用價值,需要的朋友可以參考下
    2015-04-04
  • C#獲取Visio模型信息的簡單方法示例

    C#獲取Visio模型信息的簡單方法示例

    這篇文章主要給大家介紹了關于C#獲取Visio模型信息的相關資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧。
    2017-11-11
  • C# WCF簡單入門圖文教程(VS2010版)

    C# WCF簡單入門圖文教程(VS2010版)

    這篇文章主要介紹了WCF簡單入門圖文教程,版本是VS2010版,幫助大家輕松學習了解DataContract、ServiceContract等特性,感興趣的小伙伴們可以參考一下
    2016-03-03
  • C#中的那些警告該如何去除(完全去除C#警告)

    C#中的那些警告該如何去除(完全去除C#警告)

    C#(英文名為 CSharp)是微軟開發(fā)的一種面向?qū)ο蟮木幊陶Z言,下面這篇文章主要給大家介紹了關于C#中的那些警告該如何去除的相關資料,文中通過圖文介紹的非常詳細,需要的朋友可以參考下
    2023-02-02
  • c# 修改windows中賬戶的用戶名和密碼

    c# 修改windows中賬戶的用戶名和密碼

    這篇文章主要介紹了c# 改變windows中賬戶的用戶名和密碼,幫助大家更好的理解和學習C#,感興趣的朋友可以了解下
    2020-11-11
  • C#基于WebSocket實現(xiàn)聊天室功能

    C#基于WebSocket實現(xiàn)聊天室功能

    這篇文章主要為大家詳細介紹了C#基于WebSocket實現(xiàn)聊天室功能,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-02-02
  • C# 超高面試題收集整理

    C# 超高面試題收集整理

    C# 超高面試題,學習c sharp的朋友可以看下,有說明地方的不足。是不是所有問題都有自己的解決方法。
    2010-03-03

最新評論