.NET進階篇-語言章-1-Generic泛型深入
內容目錄
一、概述二、泛型的好處三、泛型使用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("************************");
五、總結
通過泛型類可以建立獨立於型別的類,泛型方法創建出獨立於型別的方法。介面、結構、委託也可以用泛型的方式建立。建議如果我們需要設計和型別無關的物件時,可以使用泛型,把鍋甩給呼叫方,由上端決定例項化具體什麼型別。
如果手機在手邊,也可以關注下vx:xishaobb,互動或獲取更多訊息。當然這裡也一直更新de。