【1】:泛型介紹
泛型是C#2.0中一個重要的新特性,泛型是CLR和程式語言提供的一種特殊機制,它支援另一種形式的程式碼重用。泛型通常用與集合以及作用於集合的方法一起使用,當然也可以單獨使用.
C#是一種強型別的語言,在泛型沒有被提出之前,我們在使用集合的程式碼的時候,每次對其進行轉換都需要隱式的強制轉換,我們都知道所有物件的最終基類是object,我們在每次使用object的時候,無論是變換什麼型別都要對其進行強制轉換。
那麼有了泛型之後,使用泛型我們就無需進行轉換,因為泛型根據接收者傳入的引數型別,直接將每個型別更改為所傳入的引數型別.
一般情況下,建立泛型類的過程為:從一個現有的具體類開始,逐一將每個型別更改為型別引數,直至達到通用化和可用性的最佳平衡。 建立您自己的泛型類時,需要特別注意以下事項:
- 將哪些型別通用化為型別引數。
通常,能夠引數化的型別越多,程式碼就會變得越靈活,重用性就越好。 但是,太多的通用化會使其他開發人員難以閱讀或理解程式碼。
- 如果存在約束,應對型別引數應用什麼約束
一條有用的規則是,應用盡可能最多的約束,但仍使您能夠處理必須處理的型別。 例如,如果您知道您的泛型類僅用於引用型別,則應用類約束。 這可以防止您的類被意外地用於值型別,並允許您對 T 使用 as 運算子以及檢查空值。
- 是否將泛型行為分解為基類和子類。
由於泛型類可以作為基類使用,此處適用的設計注意事項與非泛型類相同。 請參見本主題後面有關從泛型基類繼承的規則。
- 是否實現一個或多個泛型介面。
例如,如果您設計一個類,該類將用於建立基於泛型的集合中的項,則可能必須實現一個介面,如 IComparable,其中 T 是您的類的型別。
【2】:泛型的表示方式
泛型可以為引用型別和值型別還有介面和委託,FCL( DLL程式集,包含了.NET框架下的各種DLL )類中定義了一個泛型列表,用來管理一個物件集合,如果我們想要使用這個泛型列表,可以在使用時指定具體資料型別。
泛型的表示為 “T” 如:List<T>, T 表示一個未指定的資料型別,我們可以看一下FCL類中泛型的引用定義:
System.Collections.Generic 名稱空間包含定義泛型集合的介面和類,泛型集合允許使用者建立強型別集合,它能提供比非泛型強型別集合更好的型別安全性和效能。
建立泛型類的過程為:從一個現有的具體類開始,逐一將每個型別更改為型別引數,直至達到通用化和可用性的最佳平衡
【3】:泛型的好處
1 : 使程式碼更加的簡潔,清晰
從前面我們也提到了,泛型具備可重用性 , 減少我們程式碼量, 使我們的程式更容易開發和維護,舉例 :
例項 1 : 在從資料庫中獲取資料庫的時候,我們經常會返回一個DataTable型別,然後將其轉換為List集合. 那麼如果沒有泛型的話,我會一般會這樣來做.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
public List<TrainingUser>GetTrainingUser(string userId) { DataTable dt = SqliteHelper.ExecuteDataset(System.Data.CommandType.Text, @" SELECT DISTINCT UserId,TrainingId FROM TRAININGUSER AS TU INNER JOIN [USER] AS U ON U.ID = TU.USERID JOIN [TRAINING] AS T ON T.ID = TU.TRAININGID WHERE U.ID = '"+userId+"' AND T.ENDTIME > DATETIME('now', 'localtime') AND T.StartTime <= DATETIME('now', 'localtime') ;").Tables[0]; return DataTableToList(dt); } private List<TrainingUser> DataTableToList(DataTabledt) { List<TrainingUser> list = new List<TrainingUser>(); if(dt. Rows.Count > 0 ) { foreach (DataRow row in dt .Rows) { TrainingUser trainingUser = new TrainingUser(); if(row["UserId" ] != null) { trainingUser .UserId = row["UserId"].ToString(); } if(row["TrainingId" ] != null) { trainingUser.TrainingId = row["TrainingId"].ToString(); } list.Add(trainingUser); } } return list; } |
在方法DataTableToList中,我們傳入了一個DataTable的物件,然後在去迴圈遍歷每一行的物件值從而去賦值給TrainingUser型別物件,這只是其中的一個方法,如果我們的型別還有 Training/User/Project等型別的話,我們是不是就要寫很多如同DataTableToList這樣的方法呢? 這就體現出了這樣的方式,會造成程式碼的冗餘以及維護不便問題,那麼我們使用泛型來解決
例項 2 : 使用泛型使上面的程式碼更見的清晰,簡潔
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
public static List<T> ToList1<T>(DataTable dt) whereT : class, new() { var prlist =new List<PropertyInfo>(); Type type = typeof(T); Array.ForEach( type.GetProperties(), p => { if(dt.Columns.IndexOf(p.Name) !=-1) { prlist.Add(p); } }); var oblist = new List<T>(); // System.Data.SqlTypes. foreach(DataRow row in dt.Rows) { var ob = new T(); prlist.ForEach( p => { if(row[p.Name] != DBNull.Value) { p.SetValue(ob, row[p.Name], null); } }); oblist.Add(ob); } return oblist; } |
在上面的這個方法中,我們定義了一個泛型方法,內部實現中是使用了反射的原理,將DataTable轉換為了List(反射後續隨筆中總結,此處只關注泛型部分即可),我們定義了一個靜態的返回值為List<T> ,前面我們說過 T : 代表任意型別(列舉除外),ToList1<T>,說明我們在呼叫這個方法的時候,同時要賦予方法名一個型別值,這個型別要和它的返回值型別一致(泛型是型別安全的),Where : 用於限制T的條件 ,例如 where T : class,new() 表示 T 只能是一個類,或者一個型別物件,那麼我們在呼叫的時候就可以這樣來
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public List<TrainingUser>GetTrainingIdByUserId(string userId) { List<TrainingUser> trainingUserList = DataTableHelper.ToList1<TrainingUser>( SqliteHelper.ExecuteDataset(System.Data.CommandType.Text, @" SELECT DISTINCT UserId,TrainingId FROM TRAININGUSER AS TU INNER JOIN [USER] AS U ON U.ID = TU.USERID JOIN [TRAINING] AS T ON T.ID = TU.TRAININGID WHERE U.ID = '"+ userId +"' AND T.ENDTIME > DATETIME('now', 'localtime') AND T.StartTime <= DATETIME('now', 'localtime') ;").Tables[0]); return trainingUserList ; } |
程式碼中的DataTableHelper.ToList1<TrainingUser> 即為我們剛才所寫的一個泛型方法,這樣我們可以看到,ToList<T> 傳入的型別為TrainingUser,同時接收者為:List<T> = List<TrainingUser> ,
這樣即便我們後續還有Training/User/Project等其他的型別,我們都可以直接使用DataTableHelper.ToList1<T>(DataTable dt) 來進行型別轉換.
2 : 提升程式的效能
泛型與非泛型相比較而言,效能要好一些,這是為什麼? 首先,泛型的型別是由呼叫者(接收者),去直接賦值的(型別安全的), 那麼就不會存在型別轉換的問題,其次, 泛型減少了裝箱和拆箱的過程.
例項 3 : 對於值型別泛型與非泛型的效能比較
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
private static void ListTest() { List<int>list = new List<int>(); for(inti = 0; i < 100; i++) { list.Add(i); int a = list[i]; } list =null; } private static void ArrListTest() { ArrayList arr = new ArrayList(); for(inti = 0; i <100; i++) { arr.Add(i); int s = (int)arr[i]; } arr = null; } Stopwatch sw = new Stopwatch(); sw.Start(); ListTest(); Console.WriteLine(" 使用泛型List執行值型別方法歷時 : "+ sw.Elapsed.ToString()); sw.Stop(); Stopwatch sw1 = new Stopwatch(); sw1.Start(); ArrListTest(); Console.WriteLine(" 使用非泛型ArrayList執行值型別方法歷時 : "+ sw1.Elapsed.ToString()); sw1.Stop(); Console.ReadLine(); |
通過迴圈 100 來比較,結果為 :
我們可以看到非泛型的時間要比泛型的時間多出0.0000523秒,泛型比非泛型的時間要多出一些, 那麼我們將數值改動一下改為迴圈 1000次.得出結果為 :
泛型比非泛型執行的時間要短0.0000405秒
我們將時間在改動一下,改為 100000呢?結果為 :
這次差距為 0.0054621 並且隨著執行次數的增長,非泛型相比泛型的時間會逐步的增加,
通過反編譯我們也能看出 :
泛型:
非泛型
從編譯中我們也能看出泛型方法中,接收的為Int32,非泛型為Object,其次泛型不會進行裝箱和拆箱操作,非泛型每次執行都要進行裝箱和拆箱操作.
3 : 型別安全
在例項1 , 2 ,3 中我們都有備註說明,泛型的傳送著必須要和接收者進行一致,否則會報異常 ,例如 :
例項 4 :
將一個泛型演算法應用於一個具體的型別時,編譯器和CLR能理解開發人員的意圖,並保證只有與指定資料型別相容的物件才能隨同演算法使用,若試圖使用不相容型別的一個物件,會造成編譯時錯誤,或者執行時丟擲異常
此篇至此,下篇主要知識點 :
1、泛型方法
2、泛型介面
3、泛型約束(主要約束,次要約束,構造器約束)
4、泛型型別轉型
5、泛型委託
6、泛型和反射
7、泛型和屬性
參考資料 :
CLR C# Via
深入理解C#
https://msdn.microsoft.com/zh-cn/library/sz6zd40f.aspx
https://msdn.microsoft.com/zh-cn/library/0x6a29h6.aspx