.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>
{
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("************************");

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