C#高級語法之泛型、泛型約束,類型安全、逆變和協變(思想原理)

1、爲何使用泛型?

泛型其實就是一個不肯定的類型,能夠用在類和方法上,泛型在聲明期間沒有明確的定義類型,編譯完成以後會生成一個佔位符,只有在調用者調用時,傳入指定的類型,纔會用確切的類型將佔位符替換掉。

首先咱們要明白,泛型是泛型,集合是集合,泛型集合就是帶泛型的集合。下面咱們來模仿這List集合看一下下面這個例子:api

咱們的目的是要寫一個能夠存聽任何動物的集合,首先抽象出一個動物類:

//動物類
public class Animal
{
    //隨便定義出一個屬性和方法
    public String SkinColor { get; set; }//皮膚顏色
    //會跑的方法
    public virtual void CanRun()
    {
        Console.WriteLine("Animal Run Can");
    }
}

而後建立Dog類和Pig類

//動物子類 Dog
public class Dog : Animal
{
    //重寫父類方法
    public override void CanRun()
    {
        Console.WriteLine("Dog Can Run");
    }
}

//動物子類 Pig
public class Pig : Animal
{
    //重寫父類方法
    public override void CanRun()
    {
        Console.WriteLine("Pig Can Run");
    }
}

由於咱們的目的是存放全部的動物,而後咱們來寫一個AnimalHouse用來存放全部動物:數組

//存放全部動物
public class AnimalHouse
{
    //因爲本身寫線性表須要考慮不少東西,並且咱們是要講泛型的,因此內部就用List來實現
    private List<Animal> animal = new List<Animal>();

    //添加方法
    public void AddAnimal(Animal a)
    {
        animal.Add(a);
    }
    //移除方法,並返回是否成功
    public bool RemoveAnimal(Animal a)
    {
        return animal.Remove(a);
    }

}

AnimalHouse類型能夠存放全部的動物,存放起來很方便。可是每次取出的話,使用起來會很不方便,由於只能用一些動物的特徵,而沒法使用子類的特徵,例如Dog子類有CanSwim()方法(會游泳的方法),而動物中是沒有這個方法的,因此就沒法進行調用,必須將Animal類型轉換爲Dog類型纔可使用,不只會增長額外的開銷並且還有很大的不肯定性,可能轉換失敗,由於AnimalHouse中是存放了不少種動物子類。安全

若是咱們有方法能夠作到,讓調用者來決定添加什麼類型(具體的類型,例如Dog、Pig),而後咱們建立什麼類型,是否是這些問題就不存在了?泛型就能夠作到。ide

咱們看一下泛型是如何定義的:函數

//用在類中
public class ClassName<CName>
{
    //用在方法中
    public void Mothed<MName>() {
        
    }

    //泛型類中具體使用CName
    //返回值爲CName而且接受一個類型爲CName類型的對象
    public CName GetC(CName c) {
        //default關鍵字的做用就是返回類型的默認值
        return default(CName);
    }
}

其中CName和MName是可變的類型(名字也是可變的),用法的話就和類型用法同樣,用的時候就把它當成具體的類型來用。測試

瞭解過泛型,接下來咱們使用泛型把AnimalHouse類更改一下,將全部類型Animal更改成泛型,以下:ui

public class AnimalHouse<T>
{
    private List<T> animal = new List<T>();

    public void AddAnimal(T a)
    {
        animal.Add(a);
    }
    public bool RemoveAnimal(T a)
    {
        return animal.Remove(a);
    }

}

AnimalHouse類型想要存儲什麼樣的動物,就能夠徹底交由調用者來決定:spa

//聲明存放全部Dog類型的集合
AnimalHouse<Dog> dog = new AnimalHouse<Dog>();
//聲明存放全部Pig類型的集合
AnimalHouse<Pig> pig = new AnimalHouse<Pig>();

調用方法的時候,本來寫的是T類型,當聲明的時候傳入具體的類型以後,類中全部的T都會變成具體的類型,例如Dog類型,Pig類型code

 

這樣咱們的問題就解決了,當調用者傳入什麼類型,咱們就構造什麼類型的集合來存放動物。對象

可是還有一個問題,就是調用者也能夠不傳入動物,調用者能夠傳入一個桌子(Desk類)、電腦(Computer),可是這些都不是咱們想要的。好比咱們須要調用動物的CanRun方法,讓動物跑一下再放入集合裏(z),由於咱們知道動物都是繼承自Animal類,全部動物都會有CanRun方法,可是若是傳入過來一個飛Desk類咱們還能使用CanRun方法嗎?答案是未知的,因此爲了確保安全,咱們須要對傳入的類型進行約束。

2、泛型約束

泛型約束就是對泛型(傳入的類型)進行約束,約束就是指定該類型必須知足某些特定的特徵,例如:能夠被實例化、好比實現Animal類等等

咱們來看一下官方文檔上都有那些泛型約束:

約束 說明
where T : struct 類型參數必須是值類型。 能夠指定除 Nullable<T> 之外的任何值類型。 有關能夠爲 null 的類型的詳細信息,請參閱能夠爲 null 的類型
where T : class 類型參數必須是引用類型。 此約束還應用於任何類、接口、委託或數組類型。
where T : unmanaged 類型參數必須是非託管類型
where T : new() 類型參數必須具備公共無參數構造函數。 與其餘約束一塊兒使用時,new() 約束必須最後指定。
where T : <基類名> 類型參數必須是指定的基類或派生自指定的基類。
where T : <接口名稱> 類型參數必須是指定的接口或實現指定的接口。 可指定多個接口約束。 約束接口也能夠是泛型。
where T : U 爲 T 提供的類型參數必須是爲 U 提供的參數或派生自爲 U 提供的參數。

 

 

 

 

 

 

 

 

對多個參數應用約束:

//微軟官方例子
class
Base { } class Test<T, U> where U : struct where T : Base, new() { }

使用的話只須要在泛型後面添加 where 泛型 : 泛型約束一、泛型約束2....,若是有new()約束的話則必須放在最後,說明都有很詳細的介紹。

而後咱們來爲AnimalHouse添加泛型約束爲:必須包含公共無參構造函數和基類必須是Animal

//Animal約束T必須是Animal的子類或者自己,new()約束放在最後
public class AnimalHouse<T> where T : Animal, new()
{
    private List<T> animal = new List<T>();

    public void AddAnimal(T a)
    {
        //調用CanRun方法
        //若是不加Animal泛型約束是沒法調用.CanRun方法的,由於類型是不肯定的
        a.CanRun();
        //添加
        animal.Add(a);
    }
    public bool RemoveAnimal(T a)
    {
        return animal.Remove(a);
    }

}

而後調用的時候咱們傳入Object試一下

提示Object類型不能傳入AnimalHouse<T>中,由於沒法轉換爲Animal類型。

咱們在寫一個繼承Animal類的Tiger子類,而後私有化構造函數

//動物子類 Tiger
public class Tiger : Animal
{
    //私有化構造函數
    private Tiger()
    {

    }
    public override void CanRun()
    {
        Console.WriteLine("Tiger Can Run");
    }
}

而後建立AnimalHouse類型對象,傳入Tiger類試一下:

提示必須是公共無參的非抽象類型構造函數。如今咱們的AnimalHouse類就很完善了,能夠存入全部的動物,並且只能存入動物

3、逆變和協變

先來看一個問題

Dog dog = new Dog();
Animal animal = dog;

這樣寫編譯是不會報錯的,由於Dog繼承了Animal,默認會進行一個隱式轉換,可是下面這樣寫

AnimalHouse<Dog> dogHouse = new AnimalHouse<Dog>();
AnimalHouse<Animal> animalHouse = dogHouse;

這樣寫的話會報一個沒法轉換類型的錯誤。

強轉的話,會轉換失敗,咱們設個斷點在後一句,而後監視一下animalHouse的值,能夠看到值爲null

//強轉編譯會經過,強轉的話會轉換失敗,值爲null
IAnimalHouse<Animal> animalHouse = dogHouse as IAnimalHouse<Animal>;

協變就是爲了解決這一問題的,這樣作其實也是爲了解決類型安全問題(百度百科):例如類型安全代碼不能從其餘對象的私有字段讀取值。它只從定義完善的容許方式訪問類型才能讀取。

由於協變只能用在接口或者委託類型中,因此咱們將AnimalHouse抽象抽來一個空接口IAnimalHouse,而後實現該接口:

//動物房子接口(全部動物的房子必須繼承該接口,例如紅磚動物房子,別墅動物房)
public interface IAnimalHouse<T> where T : Animal,new()
{

}
//實現IAnimalHouse接口
public class AnimalHouse<T> : IAnimalHouse<T> where T : Animal,new()
{
    private List<T> animal = new List<T>();

    public void AddAnimal(T a)
    {
        a.CanRun();
        animal.Add(a);
    }
    public bool RemoveAnimal(T a)
    {
        return animal.Remove(a);
    }
}

協變是在T泛型前使用out關鍵字,其餘不須要作修改

public interface IAnimalHouse<out T> where T : Animal,new()
{

}

接下來咱們用接口來調用一下,如今一切ok了,編譯也能夠經過

IAnimalHouse<Dog> dogHouse = new AnimalHouse<Dog>();
IAnimalHouse<Animal> animalHouse = dogHouse;

協變的做用就是能夠將子類泛型隱式轉換爲父類泛型,而逆變就是將父類泛型隱式轉換爲子類泛型

將接口類型改成使用in關鍵字

public interface IAnimalHouse<in T> where T : Animal,new()
{

}

逆變就完成了:

IAnimalHouse<Animal> animalHouse = new AnimalHouse<Animal>();
IAnimalHouse<Dog> dogHouse = animalHouse;

逆變和協變還有兩點:協變時泛型沒法做爲參數、逆變時泛型沒法做爲返回值。

逆變:

協變:

語法都是一些 很是粗糙的東西,重要的是思想、思想、思想。而後咱們來看一下爲何要有逆變和協變?

什麼叫作類型安全?C#中的類型安全我的理解大體就是:一個對象向父類轉換時,會隱式安全的轉換,而兩種不肯定能夠成功轉換的類型(父類轉子類),轉換時必須顯式轉換。解決了類型安全大體就是,這兩種類型必定能夠轉換成功。(若是有錯誤,歡迎指正)。

 

協變的話我相信應該很好理解,將子類轉換爲父類,兼容性好,解決了類型安全(由於子類轉父類是確定能夠轉換成功的);而協變做爲返回值是百分百的類型安全

 

「逆變爲何又是解決了類型安全呢?父類轉子類也安全嗎?不是有可能存在失敗嗎?」

其實逆變的內部也是實現子類轉換爲父類,因此說也是安全的。

「但是我明明看到的是IAnimalHouse<Dog> dogHouse = animalHouse;將父類對象賦值給了子類,你還想騙人?」

這樣寫確實是將父類轉換爲子類,不過逆變是用在做爲參數傳遞的。這是由於寫代碼的「視角」緣由,爲何協變這麼好理解,由於子類轉換父類很明顯可一看出來「IAnimalHouse<Animal> animalHouse = dogHouse;」,而後咱們換個「視角」,將逆變做爲參數傳遞一下,看這個例子:

 

先將IAnimalHouse接口修改一下:

public interface IAnimalHouse<in T> where T : Animal,new()
{
    //添加方法
    void AddAnimal(T a);
    //移除方法
    bool RemoveAnimal(T a);
}

而後咱們在主類(Main函數所在的類)中添加一個TestIn方法來講明爲何逆變是安全的:

//須要一個IAnimalHouse<Dog>類型的參數
public void TestIn(IAnimalHouse<Dog> dog) {
    
}

接下來咱們將「視角」切到TestIn中,做爲第一視角,咱們正在寫這個方法,至於其餘人如何調用咱們都是不得而知的

咱們就隨便在當前方法中添加一個操做:爲dog變量添加一個Dog對象,TestIn方法改成以下:

//須要一個IAnimalHouse<Dog>類型的參數
public static void TestIn(IAnimalHouse<Dog> dog) {
    Dog d = new Dog();
    dog.AddAnimal(d);
}

咱們將「視角」調用者視角,若是咱們想調用當前方法,只有兩種方法:

//第一種
AnimalHouse<Dog> dogHouse = new AnimalHouse<Dog>();
TestIn(dogHouse);
//第二種 
AnimalHouse<Animal> animalHouse = new AnimalHouse<Animal>();
//由於使用了in關鍵字因此能夠傳入父類對象
TestIn(animalHouse);

第一種的話咱們就不看了,很正常也很合理,咱們主要來看第二種,那第二種類型安全又在哪兒呢?

可能有人已經反應過來了,咱們再來看一下TestIn方法,有一個須要傳遞過來的IAnimalHouse<Dog>類型的dog對象,若是調用者是使用第二種方法調用的,那這個所謂的IAnimalHouse<Dog>類型的dog對象是否是其實就是AnimalHouse<Animal>類型的對象?而dog.AddAnimal(參數類型);的參數類型是否是就是須要一個Animal類型的對象?那傳入一個Dog類型的d對象是否是最終也是轉換爲Animal類型放入dog對象中?因此當逆變做爲參數傳遞時,類型是安全的。

思考:那麼,如今你能明白上面那個錯誤,爲何「協變時泛型沒法做爲參數、逆變時泛型沒法做爲返回值」了嗎?

public interface IAnimalHouse<in T> where T : Animal,new()
{
    //若是這樣寫逆變成立的話
    //咱們實現該接口,實現In方法,return(返回)一個默認值default(T)或者new T()
    //此時使用第二種方法調用TestIn,並在TestIn中調用In方法
    //注意,在TestIn中In方法的顯示返回值確定是Dog,可是實際上要返回的類型是Animal
    //因此就存在Animal類型轉換爲Dog類型,因此就有可能失敗
    //因此逆變時泛型沒法做爲返回值
    T In();

    void AddAnimal(T a);
    bool RemoveAnimal(T a);
}
逆變思考答案,建議本身認真思考事後再看
//在主類(Main類)中添加一個out協變測試方法
public static IAnimalHouse<Animal> TestOut() {
    //返回一個子類
    return new AnimalHouse<Dog>();
}

//回到接口
public interface IAnimalHouse<out T> where T : Animal,new()
{
    //若是這樣寫協變成立的話
    //咱們在Main方法中調用TestOut()方法,使用house變量接收一下
    //IAnimalHouse<Animal> house = TestOut();
    //而後調用house的AddAnimal()方法
    //注意,此時AddAnimal方法須要的是一個Animal,可是實際類型倒是Dog類型
    //由於咱們的TestOut方法返回的是一個Dog類型的對象
    //因此當咱們在AddAnimal()中傳入new Animal()時,會存在Animal父類到Dog子類的轉換
    //類型是不安全的,因此協變時泛型沒法做爲參數
    void AddAnimal(T a);
    bool RemoveAnimal(T a);
}
協變思考答案,建議本身認真思考事後再看

若是我哪點講的有誤或者那點不是太明白均可以留言指正或提問。

相關文章
相關標籤/搜索