最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

C#中逆變的實(shí)際應(yīng)用場景詳解

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

前言

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

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

協(xié)變的應(yīng)用場景

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

其中最常見的應(yī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類型,DogAnimal的子類,其他人在調(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)用場景

提起逆變,可能大家見過類似下面這段代碼:

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)用場景呢?

要探索逆變可以幫助我們解決哪些問題,我們?cè)囍鴱牧硪粋€(gè)角度出發(fā)——在某個(gè)場景下,不使用逆變,是否會(huì)遇到某些問題。

假設(shè)我們需要保存各種基礎(chǔ)資料,根據(jù)需求我們定義了對(duì)應(yīng)的接口,以及完成了對(duì)應(yīng)接口的實(shí)現(xiàn)。這里假設(shè)AnimalHuman就是其中的兩種基礎(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)聽進(jìn)度代碼
        }
    }
    public void BatchSaveHuman(IEnumerable<Human> entities)
    {
        foreach (var human in entities)
        {
            _humanSvc.Save(human);
            // 省略監(jiān)聽進(jìn)度代碼
        }
    }
}

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

為了使代碼能夠復(fù)用,我們需要抽象出一個(gè)保存基礎(chǔ)資料的接口ISave<T>。

使IAnimalService、IHumanService繼承ISave<T>,將泛型T分別定義為AnimalHuman

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ì)在無對(duì)應(yīng)接口實(shí)現(xiàn)時(shí)拋出錯(cuò)誤

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

重構(gòu)后的代碼達(dá)到了可復(fù)用、易維護(hù)的目的,但很快你會(huì)發(fā)現(xià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)槲覀儾]有實(shí)現(xiàn)ISave<Dog>接口。

雖然DogAnimal的子類,但卻不能使用保存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);// 由于沒有實(shí)現(xiàn)ISave<Dog>接口,因此代碼運(yùn)行時(shí)會(huì)報(bào)錯(cuò)
}

TDog時(shí),要想獲取ISave<Animal>這個(gè)不相關(guān)的服務(wù),我們可以從IServiceCollection服務(wù)集合中去找。

雖然我們拿到了注冊(cè)的所有服務(wù),但如何才能在TDog類型時(shí),拿到對(duì)應(yīng)的ISave<Animal>服務(wù)呢?

這時(shí),逆變就派上用場了,我們將接口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)聽進(jìn)度代碼
        }
    }
}

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

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

  • 缺點(diǎn):如果傳入的參數(shù)沒有對(duì)應(yīng)的接口實(shí)現(xiàn),編譯仍然會(huì)通過,只有在代碼運(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,用來指定調(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)聽進(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ù),使用起來會(huì)有些麻煩。

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

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

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

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

直白的理解: 

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

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

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

討論

以上是我遇見的比較常見的關(guān)于逆變的應(yīng)用場景,上述兩種方式你覺得哪種更好?是否有更好的設(shè)計(jì)方式?或者大家在寫代碼時(shí)遇見過哪些逆變的應(yīng)用場景?

總結(jié)

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

相關(guān)文章

最新評(píng)論

长丰县| 丹棱县| 巧家县| 泌阳县| 库尔勒市| 大邑县| 洮南市| 桦川县| 平湖市| 依兰县| 寿阳县| 永昌县| 江门市| 阳高县| 武宣县| 乐亭县| 钟山县| 桓台县| 榆林市| 寿宁县| 夹江县| 滦平县| 宁德市| 临潭县| 梨树县| 紫金县| 丹棱县| 宁津县| 宣汉县| 南川市| 宿松县| 南部县| 尼勒克县| 白玉县| 佛冈县| 嫩江县| 巍山| 慈溪市| 阳西县| 西宁市| 桐柏县|