天天看點

android 移除泛型中元素_C#面試技巧之泛型、泛型限制,類型安全、逆變和協變(思想原理)...

一、為什麼使用泛型?

泛型其實就是一個不确定的類型,可以用在類和方法上,泛型在聲明期間沒有明确的定義類型,編譯完成之後會生成一個占位符,隻有在調用者調用時,傳入指定的類型,才會用确切的類型将占位符替換掉。

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

我們的目的是要寫一個可以存放任何動物的集合,首先抽象出一個動物類:

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

然後建立Dog類和Pig類

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

因為我們的目的是存放所有的動物,然後我們來寫一個AnimalHouse用來存放所有動物:

//存放所有動物public class AnimalHouse{    //由于自己寫線性表需要考慮很多東西,而且我們是要講泛型的,是以内部就用List來實作    private List animal = new List();    //添加方法    public void AddAnimal(Animal a)    {        animal.Add(a);    }    //移除方法,并傳回是否成功    public bool RemoveAnimal(Animal a)    {        return animal.Remove(a);    }}
           

AnimalHouse類型可以存放所有的動物,但是每次存入子類對象的時候就會進行裝箱操作,每次取出的話,還要再次進行拆箱操作,會消耗額外的性能,因為所有的子類都能存放,是以拆箱的話也會很麻煩。

如果我們有方法可以做到,讓調用者來決定添加什麼類型(具體的類型,例如Dog、Pig),然後我們建立什麼類型,是不是這些問題就不存在了?泛型就可以做到。

我們看一下泛型是如何定義的:

//用在類中public class ClassName{    //用在方法中    public void Mothed() {            }    //泛型類中具體使用CName    //傳回值為CName并且接受一個類型為CName類型的對象    public CName GetC(CName c) {        //default關鍵字的作用就是傳回類型的預設值        return default(CName);    }}
           

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

了解過泛型,接下來我們使用泛型把AnimalHouse類更改一下,将所有類型Animal更改為泛型,如下:

public class AnimalHouse{    private List animal = new List();    public void AddAnimal(T a){        animal.Add(a);    }    public bool RemoveAnimal(T a){        return animal.Remove(a);    }}
           

AnimalHouse類型想要存儲什麼樣的動物,就可以完全交由調用者來決定:

//聲明存放所有Dog類型的集合AnimalHouse dog = new AnimalHouse();//聲明存放所有Pig類型的集合AnimalHouse pig = new AnimalHouse();
           

調用方法的時候,原本寫的是T類型,當聲明的時候傳入具體的類型之後,類中所有的T都會變成具體的類型,例如Dog類型,Pig類型

android 移除泛型中元素_C#面試技巧之泛型、泛型限制,類型安全、逆變和協變(思想原理)...
android 移除泛型中元素_C#面試技巧之泛型、泛型限制,類型安全、逆變和協變(思想原理)...

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

但是還有一個問題,就是調用者也可以不傳入動物,調用者可以傳入一個桌子(Desk類)、電腦(Computer),但是這些都不是我們想要的。比如我們需要調用動物的CanRun方法,讓動物跑一下再放入集合裡(z),因為我們知道動物都是繼承自Animal類,所有動物都會有CanRun方法,但是如果傳入過來一個飛Desk類我們還能使用CanRun方法嗎?答案是未知的,是以為了確定安全,我們需要對傳入的類型進行限制。

二、泛型限制

泛型限制就是對泛型(傳入的類型)進行限制,限制就是指定該類型必須滿足某些特定的特征,例如:可以被執行個體化、比如實作Animal類等等

我們來看一下官方文檔上都有那些泛型限制:

限制 說明
where T : struct 類型參數必須是值類型。 可以指定除 Nullable 以外的任何值類型。 有關可以為 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    where U : struct    where T : Base, new(){ }
           

使用的話隻需要在泛型後面添加 where 泛型 : 泛型限制1、泛型限制2....,如果有new()限制的話則必須放在最後,說明都有很詳細的介紹。

然後我們來為AnimalHouse添加泛型限制為:必須包含公共無參構造函數和基類必須是Animal

//Animal限制T必須是Animal的子類或者本身,new()限制放在最後public class AnimalHousewhere T : Animal, new(){    private List animal = new List();    public void AddAnimal(T a){        //調用CanRun方法        //如果不加Animal泛型限制是無法調用.CanRun方法的,因為類型是不确定的        a.CanRun();        //添加        animal.Add(a);    }    public bool RemoveAnimal(T a){        return animal.Remove(a);    }}
           

然後調用的時候我們傳入Object試一下

android 移除泛型中元素_C#面試技巧之泛型、泛型限制,類型安全、逆變和協變(思想原理)...

提示Object類型不能傳入AnimalHouse中,因為無法轉換為Animal類型。

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

//動物子類 Tigerpublic class Tiger : Animal{    //私有化構造函數    private Tiger()    {    }    public override void CanRun()    {        Console.WriteLine("Tiger Can Run");    }}
           

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

android 移除泛型中元素_C#面試技巧之泛型、泛型限制,類型安全、逆變和協變(思想原理)...

提示必須是公共無參的非抽象類型構造函數。現在我們的AnimalHouse類就很完善了,可以存入所有的動物,而且隻能存入動物

三、逆變和協變

先來看一個問題

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

這樣寫編譯是不會報錯的,因為Dog繼承了Animal,預設會進行一個隐式轉換,但是下面這樣寫

AnimalHouse<Dog> dogHouse = new AnimalHouse<Dog>();AnimalHouse<Animal> animalHouse = dogHouse;
           
android 移除泛型中元素_C#面試技巧之泛型、泛型限制,類型安全、逆變和協變(思想原理)...

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

強轉的話,會轉換失敗,我們設個斷點在後一句,然後監視一下animalHouse的值,可以看到值為null

//強轉編譯會通過,強轉的話會轉換失敗,值為nullIAnimalHouse animalHouse = dogHouse as IAnimalHouse;
           
android 移除泛型中元素_C#面試技巧之泛型、泛型限制,類型安全、逆變和協變(思想原理)...
android 移除泛型中元素_C#面試技巧之泛型、泛型限制,類型安全、逆變和協變(思想原理)...

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

因為協變隻能用在接口或者委托類型中,是以我們将AnimalHouse抽象抽來一個空接口IAnimalHouse,然後實作該接口:

//動物房子接口(所有動物的房子必須繼承該接口,例如紅磚動物房子,别墅動物房)public interface IAnimalHouse<T> where T : Animal,new(){}//實作IAnimalHouse接口public class AnimalHouse<T> : IAnimalHouse<T> where T : Animal,new(){    private List animal = new List();    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;
           

逆變和協變還有兩點:協變時泛型無法作為參數、逆變時泛型無法作為傳回值。

逆變:

android 移除泛型中元素_C#面試技巧之泛型、泛型限制,類型安全、逆變和協變(思想原理)...

協變:

android 移除泛型中元素_C#面試技巧之泛型、泛型限制,類型安全、逆變和協變(思想原理)...

文法都是一些 非常粗糙的東西,重要的是思想、思想、思想。然後我們來看一下為什麼要有逆變和協變?

什麼叫做類型安全?C#中的類型安全個人了解大緻就是:一個對象向父類轉換時,會隐式安全的轉換,而兩種不确定可以成功轉換的類型(父類轉子類),轉換時必須顯式轉換。解決了類型安全大緻就是,這兩種類型一定可以轉換成功。(如果有錯誤,歡迎指正)。

協變的話我相信應該很好了解,将子類轉換為父類,相容性好,解決了類型安全(因為子類轉父類是肯定可以轉換成功的);而協變作為傳回值是百分百的類型安全

“逆變為什麼又是解決了類型安全呢?子類轉父類也安全嗎?不是有可能存在失敗嗎?”

其實逆變的内部也是實作子類轉換為父類,是以說也是安全的。

“可是我明明看到的是IAnimalHouse dogHouse = animalHouse;将父類對象指派給了子類,你還想騙人?”

這樣寫确實是将父類轉換為子類,不過逆變是用在作為參數傳遞的。這是因為寫代碼的“視角”原因,為什麼協變這麼好了解,因為子類轉換父類很明顯可一看出來“IAnimalHouse animalHouse = dogHouse;”,然後我們換個“視角”,将逆變作為參數傳遞一下,看這個例子:

先将IAnimalHouse接口修改一下:

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

然後我們在主類(Main函數所在的類)中添加一個TestIn方法來說明為什麼逆變是安全的:

//需要一個IAnimalHouse類型的參數public void TestIn(IAnimalHouse dog) {    }
           

接下來我們将“視角”切到TestIn中,作為第一視角,我們正在寫這個方法,至于其他人如何調用我們都是不得而知的

我們就随便在目前方法中添加一個操作:為dog變量添加一個Dog對象,TestIn方法改為如下:

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

我們将“視角”調用者視角,如果我們想調用目前方法,隻有兩種方法:

//第一種AnimalHouse dogHouse = new AnimalHouse();TestIn(dogHouse);//第二種 AnimalHouse animalHouse = new AnimalHouse();//因為使用了in關鍵字是以可以傳入父類對象TestIn(animalHouse);
           

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

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

思考:那麼,現在你能明白上面那個錯誤,為什麼“協變時泛型無法作為參數、逆變時泛型無法作為傳回值”了嗎?

public interface IAnimalHouse 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 TestOut() {    //傳回一個子類    return new AnimalHouse();}//回到接口public interface IAnimalHouse where T : Animal,new(){    //如果這樣寫協變成立的話    //我們在Main方法中調用TestOut()方法,使用house變量接收一下    //IAnimalHouse house = TestOut();    //然後調用house的AddAnimal()方法    //注意,此時AddAnimal方法需要的是一個Animal,但是實際類型卻是Dog類型    //因為我們的TestOut方法傳回的是一個Dog類型的對象    //是以當我們在AddAnimal()中傳入new Animal()時,會存在Animal父類到Dog子類的轉換    //類型是不安全的,是以協變時泛型無法作為參數    void AddAnimal(T a);    bool RemoveAnimal(T a);}協變思考答案,建議自己認真思考過後再看
           

繼續閱讀