1. 程式人生 > >C#高階語法之泛型、泛型約束,型別安全、逆變和協變(思想原理)

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

一、為什麼使用泛型?

泛型其實就是一個不確定的型別,可以用在類和方法上,泛型在宣告期間沒有明確的定義型別,編譯完成之後會生成一個佔位符,只有在呼叫者呼叫時,傳入指定的型別,才會用確切的型別將佔位符替換掉。

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

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

//動物類
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、Pig),然後我們建立什麼型別,是不是這些問題就不存在了?泛型就可以做到。

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

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

    //泛型類中具體使用CName
    //返回值為CName並且接受一個型別為CName型別的物件
    public CName GetC(CName c) {
        //default關鍵字的作用就是返回型別的預設值
        return default(CName);
    }
}

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

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

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型別想要儲存什麼樣的動物,就可以完全交由呼叫者來決定:

//宣告存放所有Dog型別的集合
AnimalHouse<Dog> dog = new AnimalHouse<Dog>();
//宣告存放所有Pig型別的集合
AnimalHouse<Pig> pig = new AnimalHouse<Pig>();

呼叫方法的時候,原本寫的是T型別,當宣告的時候傳入具體的型別之後,類中所有的T都會變成具體的型別,例如Dog型別,Pig型別

 

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

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

二、泛型約束

泛型約束就是對泛型(傳入的型別)進行約束,約束就是指定該型別必須滿足某些特定的特徵,例如:可以被例項化、比如實現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 泛型 : 泛型約束1、泛型約束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類就很完善了,可以存入所有的動物,而且只能存入動物

三、逆變和協變

先來看一個問題

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);
}
協變思考答案,建議自己認真思考過後再看

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