.NET Core CSharp初級篇 1-8泛型、逆變與協變

.NET Core CSharp初級篇 1-8

本節內容爲泛型git

爲何須要泛型

泛型是一個很是有趣的東西,他的出現對於減小代碼複用率有了很大的幫助。好比說遇到兩個模塊的功能很是類似,只是一個是處理int數據,另外一個是處理string數據,或者其餘自定義的數據類型,但咱們沒有辦法,只能分別寫多個方法處理每一個數據類型,由於方法的參數類型不一樣。有沒有一種辦法,在方法中傳入通用的數據類型,這樣不就能夠合併代碼了嗎?github

泛型簡介

在咱們的C#中,使用泛型對容許您延遲編寫類或方法中的編程元素的數據類型的規範,直到實際在程序中使用它的時候。換句話說,泛型容許您編寫一個能夠與任何數據類型一塊兒工做的類或方法。泛型的定義很是簡單,在類或函數名後使用 做爲佔位符便可,這個T也能夠換成其餘的字母代替。 編程

注意:屬性和索引器不能指定本身的泛型參數,它們只能使用所屬類中定義的泛型參數進行操做。
你能夠經過下面這個例子獲得一些關於泛型定義的方法。數組

值得注意的是泛型是在運行時進行動態變化,並非在編譯時發生。安全

泛型類與泛型函數

泛型類和泛型函數在使用上基本上是同樣的,只不過定義後的範圍不同。對於泛型類,泛型的範圍是整個類,泛型函數則是在函數內部。ide

例如這個例子函數

class A<T>
{
    public T getSomething<X>(X m,T n)
    {
        return n;
    }
    public static U test<U>(U x)
    {
        return x;
    }
}
// 實例泛型類必須指定類型
A<string> a = new A<string>()
//泛型推斷
A.test<int>(1);//原式
A.test(1);//推斷

在泛型函數的調用中,有一個語法糖,它就是泛型類型推斷。這很是好理解,C#的編譯器足夠聰明,它能夠根據你傳入的參數類型,調用gettype方法進行類型的推斷。所以你能夠在泛型函數中不顯式的指定類型。
類型推理的相同規則適用於靜態方法和實例方法。 編譯器可基於傳入的方法參數推斷類型參數;而沒法僅根據約束或返回值推斷類型參數。 所以,類型推理不適用於不具備參數的方法。 類型推理髮生在編譯時,以後編譯器嘗試解析重載的方法簽名。 編譯器將類型推理邏輯應用於共用同一名稱的全部泛型方法。 在重載解決方案步驟中,編譯器僅包含在其上類型推理成功的泛型方法。ui

泛型的範圍則是包含關係。包含在泛型類中的泛型函數能夠自由的訪問泛型類中的泛型,可是類不能夠訪問泛型函數中指定的泛型。spa

泛型約束

若是咱們使用了泛型,那麼一定面臨的一個問題就是權限問題。例如class A ,假定我但願某些類型不能夠做爲泛型傳入,那麼咱們就應當使用咱們的泛型約束。
泛型約束的使用以下例:
code

class A : T where T:class
{
    
}

泛型約束一般有下面幾類:

  • where T : struct:類型參數必須是值類型。能夠指定除 Nullable 之外的任何值類型。
  • where T : class 類型參數必須是引用類型。 此約束還應用於任何類、接口、委託或數組類型。
  • where T : unmanaged 類型參數不能是引用類型,而且任何嵌套級別均不能包含任何引用類型成員。
  • where T : new() 類型參數必須具備公共無參數構造函數。 與其餘約束一塊兒使用時,new() 約束必須最後指定。
  • where T : <基類名> 類型參數必須是指定的基類或派生自指定的基類。
  • where T : <接口名稱> 類型參數必須是指定的接口或實現指定的接口。 可指定多個接口約束。 約束接口也能夠是泛型。
  • where T : U 爲 T 提供的類型參數必須是爲 U 提供的參數或派生自爲 U 提供的參數。

某些約束是互斥的。 全部值類型必須具備可訪問的無參數構造函數。 struct 約束包含 new() 約束,且 new() 約束不能與 struct 約束結合使用。 unmanaged 約束包含 struct 約束。 unmanaged 約束不能與 struct 或 new() 約束結合使用。使用的時候稍加註意便可。

你也能夠指定多個類型佔位符,而且單獨爲他們進行約束,如:

class A<T,U> 
        where T:struct
        where U:class

甚至你能夠進行泛型自我約束,例如:

class A<T,U,K> 
        where T:struct
        where U:K

協變和逆變

這三個名詞來自於數學和物理,不少初學者都難以理解這些名詞。但事實上在C#上,這些詞是用於標示類型與類型之間的綁定。可變性是以一種類型安全的方式,將一個對象當作另外一個對象來使用。若是不能將一個類型替換爲另外一個類型,那麼這個類型就稱之爲:不變。

協變

若是某個返回的類型能夠由其派生類型替換,那麼這個類型就是支持協變的。直白的說,協變就是合理的變化,例如貓->動物,這個看上去絲毫沒有問題。這就是協變,從小變大。

例如:

// Cat:Animal
//這種變化毫無問題
Cat c = new Cat();
Animal a = c;
//報錯,由於List<Cat>不繼承於List<Animal>
List<Cat> d = new List<Cat>();
List<Animal> = d;

對於泛型的參數,咱們可使用到咱們以前講函數參數的時候所遇到的 in,out 關鍵字。In表明輸入,體現的就是逆變,Out表明輸出,表明的是協變。對於Out輸出的東西,天然不能夠對他進行輸入操做,他只能做爲結果返回,所以它不會被修改。所以進行隱式轉換的時候,編譯器認爲該轉換是安全的(返回值不變)。

例如

IEnumerable<Cat> c = new List<Cat>();

    IEnumerable<Animal> a = c;

不少人可能不不能很好地理解這些來自於物理和數學的名詞。咱們無需去了解他們的數學定義,可是至少應該能分清協變和逆變。實際上這個詞來源於類型和類型之間的綁定。咱們從數組開始理解。數組其實就是一種和具體類型之間發生綁定的類型。數組類型Int32[]就對應於Int32這個本來的類型。任何類型T都有其對應的數組類型T[]。那麼咱們的問題就來了,若是兩個類型T和U之間存在一種安全的隱式轉換,那麼對應的數組類型T[]和U[]之間是否也存在這種轉換呢?這就牽扯到了將本來類型上存在的類型轉換映射到他們的數組類型上的能力,這種能力就稱爲「可變性(Variance)」。在.NET世界中,惟一容許可變性的類型轉換就是由繼承關係帶來的「子類引用->父類引用」轉換。舉個例子,就是String類型繼承自Object類型,因此任何String的引用均可以安全地轉換爲Object引用。咱們發現String[]數組類型的引用也繼承了這種轉換能力,它能夠轉換成Object[]數組類型的引用,數組這種與原始類型轉換方向相同的可變性就稱做協變

逆變

逆變則偏偏與協變徹底相反,逆變是指代類型往更小的派生類中進行轉換,顯然這有多是不安全的,由於有可能會致使數據的丟失。在C#中使用逆變式的方法是使用In關鍵字,這意味着這個參數只能做爲返回值返回,那麼咱們就有可能對傳入的參數進行修改,所以使用強制轉換有多是不合法的。
例如:

public interface IMyList<in T>
{
    T GetElement();
    void ChangeT(T t);
}

public class MyList<T> : IMyList<T>
{
    public T GetElement()
    {
        return default(T);
    }
    public void ChangeT(T t)
    {
        //Change T
    }
}

這段代碼沒法經過編譯,由於GetElement是將T返回,這顯然不符合逆變的定義。逆變的參數只容許輸入而不容許輸出。

對於逆變的實踐,各位能夠去參閱下IList接口與IEnumerable接口的實現。這兩個接口很好的體現了在集合中的逆變與協變。

(缺乏舉例說明,後續補上)

總結

對於泛型,並無太多的奇技淫巧可言,由於泛型的出現已經就是一個奇技淫巧了。泛型最經常使用的地方是泛型數組。而且C#對於不肯定類型和大小的數組會使用一個很是好用的類,叫作List類,咱們將會在中級篇中進行詳細的介紹。

若是個人文章幫助了您,請您在github .NET Core Guide項目幫我點一個star,在博客園中點一個關注和推薦。

Github

BiliBili主頁

WarrenRyan's Blog

博客園

相關文章
相關標籤/搜索