.NET進階篇-語言章-1-Generic泛型深入

  • 2019 年 10 月 8 日
  • 筆記

內容目錄

一、概述二、泛型的好處三、泛型使用1、泛型方法2、泛型類、泛型接口四、泛型的功能1、泛型中的默認值2、約束3、協變逆變5、泛型委託4、泛型緩存五、總結

一、概述

泛型我們一定都用過,最常見的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>
{
    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("************************");

五、總結

通過泛型類可以創建獨立於類型的類,泛型方法創建出獨立於類型的方法。接口、結構、委託也可以用泛型的方式創建。建議如果我們需要設計和類型無關的對象時,可以使用泛型,把鍋甩給調用方,由上端決定實例化具體什麼類型。