.NET進階篇01-Generic泛型深入

jiaxibei96發表於2019-10-26

知識只有經過整理才能形成技能

一、概述

泛型我們一定都用過,最常見的List<T>集合。.NET2.0開始支援泛型,建立的目的就是為了不同型別建立相同的方法或類,也包括介面,委託的泛型。比如常見的ORM對映,一個方法通過傳入不同的類,返回不同的類例項,再呼叫時才確定引數型別。

我們知道想要一個類相同名稱的方法,如果僅引數型別不同,那麼要過載。過載會有很多冗餘的程式碼。在.NET1.0時代也可以不用過載,那就是引數型別直接用Object型別,那麼任何型別都能傳進去了,但是會有裝箱拆箱操作,影響效能。

public static void Show(string sValue)
{
    Console.WriteLine(sValue);
}

public static void Show(int iValue)
{
    Console.WriteLine(iValue);
}

public static void Show(object oValue)
{
    Console.WriteLine(oValue);
}
複製程式碼

二、泛型的好處

值型別和引用型別的裝箱拆箱消耗。值型別分配線上程棧上,引用型別分配在堆上,只把指標放在棧上。如圖所示,如果把int型別1裝箱,就要把1拷貝到堆中,就會有記憶體的交換。以前的ArrayList就是型別不安全的,需要頻繁的進行裝拆箱操作,Add元素的時候全部裝箱object,取的時候要拆箱,效能損失比較大。

泛型的效率等同於硬編碼的方式,就是和你很多功能相同的類效率差不多。泛型每個型別只例項化一次,下面泛型快取會詳細解讀下。先簡單介紹下CLR的執行原理(詳細在CLR章節)以瞭解泛型的原理機制。

.NET編譯器和直譯器兩階段,我們先經過編譯器編譯成IL中間語言(dll、exe),和java的位元組碼類似,然後經過JIT解釋成機器碼。這樣做的好處就是我們只需要編譯成IL後,在各個不同計算機系統上,只要有對應的CLR(JIT)就行,這樣就和平臺無關。二次編譯:為了一次編譯,不同平臺使用。泛型在第一個編譯時會用一個佔位符代替,在第二次執行時會編譯成具體的型別。所以效能相當於硬編碼的方式,每種型別最終都有自己的機器碼。

List<T>是在使用時定義型別,JIT編譯器解析時動態的生成,如定義List<int>,在JIT執行時就聲稱List<int>型別,然後操作就不會出現裝箱拆箱,而且只能新增指定的型別,這就型別安全

三、泛型使用

1、泛型方法

常見的泛型方法就是在方法後面帶上<T>(T param),“T”可以隨便定義,只要不是關鍵保留字就行,預設約定俗成都用T,此處就代表你定義了一個T類,然後後面引數就可以用這個T型別。(如果把滑鼠游標放在引數型別T上,然後F12轉到定義就會定位到前面這個T。)這樣就可以用一個方法,滿足不同的引數型別,去做相同的事情。把引數的型別申明推遲到呼叫時,延遲宣告。後面框架中也會有很多這種延遲思想,延遲以達到更好的擴充套件。

public static void Show<T>(T tValue)
{
    Console.WriteLine(tValue);
}
CommonMethod.Show<int>(123);
複製程式碼

2、泛型類、泛型介面

建立方法類似,語法一樣<T>。用的最多的List<T>就是很典型的泛型類,用來滿足不同的具體型別,完成相同的事情

public class GenericClass<T>
{
    public T _T;
}

public interface IGenericInterface<T>
{
    T GetT();
}
複製程式碼

四、泛型的功能

1、泛型中的預設值

既然用了泛型,那麼在內部想要初始化怎麼辦呢?因為泛型進來的型別不一定是值型別或引用型別,所以初始化就不能簡單直接賦null。這個時候需要用到default關鍵字,用於將泛型型別初始化為null或其他值型別預設值(0,0001/1/1 0:00:00日期等);

2、約束

泛型導致任何型別都可以進來,那麼如何去使用這個型別T,編寫的時候我們是不知道T是什麼,也不知道它能幹什麼。一個方法就是可以用反射,任何一個型別通過發射都能獲取內部的結構屬性方法呼叫。泛型約束提供更簡便的方法。在宣告泛型時在引數後面追加where關鍵字。約束可以同時指定多個,像這樣where:T People,IWork,new()。同時約束傳進來的型別People或其子類,並且繼承了IWork介面,有無引數建構函式。

public static void Show<T>(T tValue) where T : People
{
    Console.WriteLine(tValue.Name);
}
複製程式碼

3、協變逆變

協變逆變就是對引數和返回值的型別進行轉換。協變用一個派生更大的類去代替某個型別(小代替大),其實就是設計原則的里氏替換原則,比如狗繼承自動物,那麼任何用動物作為引數型別的地方,呼叫時都可以用狗代替。逆變就是反過來。

//協變
public void ShowName(Animal animal)
{
}

ShowName(dog);
複製程式碼

泛型介面的協變逆變。如果泛型型別用了out關鍵字標註,泛型介面就是協變的。這也意味著返回型別只能是T。如果用了in關鍵字標註,就是逆變,只能把泛型型別T用作方法的輸入。這塊很繞,實際使用非常少。

//一堆狗肯定是一堆動物啊,為啥就不能這麼做呢?下面這句編譯不通過
//前後兩個型別是沒有父子關係的
List<Animal> animalLst = new List<Dog>();
//下面這句就可以呢?
IEnumerable<Animal> animalLst2 = new List<Dog>();

//因為在介面中新增了out關鍵字
public interface IEnumerable<out T> : IEnumerable
{
    //
    // 摘要:
    //     Returns an enumerator that iterates through the collection.
    //
    // 返回結果:
    //     An enumerator that can be used to iterate through the collection.
    IEnumerator<T> GetEnumerator();
}

ICustomListIn<Dog> customLstIn = new CustomListIn<Animal>();

public interface ICustomListIn<in T>
{
    void Show(T t);
}

public class CustomListIn<T> : ICustomListIn<T>
{
    public void Show(T t)
    {
        Console.WriteLine(typeof(T).FullName);
    }
}

interface ISetData<in T>  //使用逆變
{
    void SetData(T data);
}

interface IGetData<out T>   //使用協變
{
    T GetData();
}

class MyTest<T> : ISetData<T>, IGetData<T>//繼承兩個泛型介面
{
    private T data;
    public void SetData(T data)
    {
        this.data = data;   //賦值
    }
    public T GetData()
    {
        return this.data;   //取資料
    }
}

MyTest<object> my = new MyTest<object>();
ISetData<string> set = my;
set.SetData("nihao");
複製程式碼

其實協變逆變就是語法糖,為了讓不是繼承關係的型別也可以互相賦值編譯通過。執行時實際右邊是什麼型別就是什麼型別。(欺騙編譯器,自己應該會很少寫協變逆變的介面或委託)

5、泛型委託

以Action為例,Action是.NET Framework內建的泛型委託,可以使用Action委託以引數形式傳遞方法,而不用顯示宣告自定義的委託。其實我們擼程式碼過程中不太需要自己定義委託,內建的Action和Func就夠用,也便於統一。Action無返回值委託,可以有16個引數,可以傳入不同的型別。在委託事件一章會詳細介紹。

4、泛型快取

泛型類的靜態成員只能在類的一個例項中共享。執行時泛型類的例項已經指定了具體型別,每一個不同的泛型類例項共享靜態成員,利用這個特點就可以做快取。每一個不同的T快取一個版本資料。如例子所示,當第一次指定不同的T時,會重新構造,再次有相同的型別時,就不會進入靜態建構函式了。相當於為快取了多個版本的靜態成員。比如在各個資料庫實體類需要有一些增刪改查的SQL時,就可以利用用泛型特性,每一個資料庫實體類都會快取一份自己的增刪改查SQL。

public class GenericCache<T>
{
    static GenericCache()
    {
        Console.WriteLine("進入靜態建構函式");
        _TypeTime = $"{typeof(T).FullName}_{DateTime.Now.ToString()}";
    }

    private static string _TypeTime = "";

    public static string GetCache()
    {
        return _TypeTime;
    }
}

Console.WriteLine("************************");
Console.WriteLine(GenericCache<int>.GetCache());
Thread.Sleep(1000);
Console.WriteLine(GenericCache<string>.GetCache());
Thread.Sleep(1000);
Console.WriteLine("認真比較列印出的靜態成員值");
Console.WriteLine(GenericCache<int>.GetCache());
Thread.Sleep(1000);
Console.WriteLine(GenericCache<string>.GetCache());
Console.WriteLine("************************");

複製程式碼

五、總結

通過泛型類可以建立獨立於型別的類,泛型方法建立出獨立於型別的方法。介面、結構、委託也可以用泛型的方式建立。建議如果我們需要設計和型別無關的物件時,可以使用泛型,把鍋甩給呼叫方,由上端決定例項化具體什麼型別。

如果手機在手邊,也可以關注下vx:xishaobb,互動或獲取更多訊息。當然這裡也一直更新de。


相關文章