1.1.1 摘要
圖1 C# 泛型介紹
在接觸泛型之前,我們編程一般都是使用具體類型(char, int, string等)或自訂類型來定義我們變數,如果我們有一個功能很強的介面,而且我們想把它提取或重構成一個通用的介面,使得該介面不僅僅適用於已定義資料類型,而是適用於更多資料類型,從而方便以後的擴充。
泛型提供上述功能的實現,泛型其實就是提供一個資料類型的抽象層,因為它泛所以抽象,方便了我們代碼的重構和提取,我們無需hard-code介面中的資料類型,而是通過一個抽象泛型型別來指定資料類型,所以泛型可以提取出一個通用的介面。接下來讓我們通過具體的例子說明什麼是泛型。
1.1.2 本文1.1.1.1 沒有泛型的C# 1.0
相信許多人都使用過棧或者其他經典資料結構,而且對於棧的FILO都是嫻熟於心了。假設現在要我們定義一個棧,用來存放具體資料類型,例如:int 資料,OK讓我們來實現自訂棧。
/// <summary>/// A custom stack1./// </summary>public class CustomStack1{ private int _stackPointer = 0; private int[] _stackArray; public void Push(int item) { ////Concrete implemention. } public int Pop() { ////Concrete implemention. }}
為了簡潔沒有完全遵守OOP的原則,這裡沒有使用屬性和給出方法的具體實現,現在我們有了第一版可以存放int資料類型的棧。但如果需求改變儲存資料類型增加string。那麼我們可以怎樣實現呢?沒錯我們只要增加一個類型為string的棧就OK了。
/// <summary>/// A custom stack2./// </summary>public class CustomStack2{ private int _stackPointer = 0; private string[] _stackArray; public void Push(int item) { ////Concrete implemention. } public string Pop() { ////Concrete implemention. }}
很快我們就完成了第二版本的棧了,這看上去的確很簡單而且實現起來很快只不過是ctrl + c和ctrl + v,但我們的代碼真的那麼簡單好維護嗎?想想其實危機四伏。我們能不能一開始就把儲存的資料類型定義成為一個通用的類型呢(抽象資料類型)?----邪惡的object。
/// <summary>/// A custom stack./// </summary>public class CustomStack{ private int _stackPointer = 0; private object[] _stackArray; public void Push(object item) { ////Concrete implemention. } public object Pop() { ////Concrete implemention. }}
現在我們實現了第三版的棧了,看上去我們好像找到了通用的解決方案,想想我們要注意一點是如果我們儲存和取出資料類型為實值型別時,實值型別要進行boxing和unboxing操作(《.NET實值型別和參考型別101》),而且還有進行資料類型檢測。如果資料量不大我們可能察覺不到效能變化,但資料量大hehehe…。這就是為什麼我們要使用泛型的原因之一,但更重要的原因是在進行boxing和unboxing時,我們需要提供更多資訊給編譯器,編譯時間再根據我們提供的資訊進行類型檢測。
1.1.1.2 C#中的泛型
現在我們知道了.NET為什麼要提供泛型,現在讓我們學習什麼是泛型和怎樣實現吧!在C# 2.0中提供了泛型這一機制,但這可不是什麼新奇的技術,因為早在C++和Java都提供泛型機制。
C#中提供五種泛型分別是:classes, structs, interfaces, delegates, and methods。
也許有人說沒有使用過C++的模板或Java的泛型,而且不瞭解C#中的泛型,真的是這樣嗎?其實我們經常都在使用泛型,相信很多人都使用過Dictionary<TKey,TValue>,沒錯這就是C#中提供的泛型字典,接下來我們將介紹自訂泛型類CustomStack。
圖2泛型類
/// <summary>/// A custom generic stack./// </summary>public class CustomStack<T>{ private int _stackPointer = 0; private T[] _stackArray; public void Push(T item) { ////Concrete implemention. } public T Pop() { ////Concrete implemention. }}
前面給出了泛型類CustomStack實現,我們發現只需在類名後添加<T>,並且把具體資料類型改為抽象的泛型型別T,現在我們就定義了一個簡單泛型類,我們可以通過以下對比一下非泛型和泛型實現的區別。
圖3非泛型和泛型類定義
通過我們發現泛型類CustomStack和非泛型類CustomStack在實現上沒有太大的區別,我們只需把具體類型換成T就OK了,接下來我們將進入泛型的進階學習。
關於委託大家可以閱讀《.NET 中的委託》和《.NET中的委託和事件(續)》,現在讓我們學習一下泛型委派。
圖4泛型委派的定義
給出了泛型委派的定義,我們要注意Type parameters包括泛型參數和傳回型別。隨著C#3.0 Linq特性的引用,泛型委派的使用得到了極大地擴充,讓我們通過具體代碼講講如何使用泛型委派。
首先我們定義一個泛型委派,然後再定義一個Calc類,接著在Calc裡定義兩個方法分別是Add() 和Divide() 如下:
/// <summary>/// Define generic delegate./// </summary>public delegate TR CalcMethod<T1, T2, TR>(T1 x, T2 y);public class Calc{ static private double sum; /// <summary> /// Gets or sets the sum. /// </summary> /// <value> /// The sum. /// </value> static public double Sum { get { return sum; } set { sum = value; } } /// <summary> /// Adds the specified x. /// </summary> /// <param name="x">The x.</param> /// <param name="y">The y.</param> /// <returns></returns> static public string Add(int x , int y) { Sum = x + y; return Sum.ToString(); } /// <summary> /// Divides the specified x. /// </summary> /// <param name="x">The x.</param> /// <param name="y">The y.</param> /// <returns></returns> static public string Divide(int x, int y) { if (y == 0) { return "除數不能為零"; } Sum = x * 1.0 / y; return Sum.ToString(); }}
接下來我們需要定義一個委託變數,而後將委託變數和具體調用方法綁定就OK了。
////Initialize engeric delegate instance to Add method.CalcMethod<int, int, string> calcMethod = new CalcMethod<int, int, string>(Calc.Add);
前面例子我們並沒有對泛型型別進行限制,由於T是一個抽象類別型,有時我們必須限制T是哪種類型,例如我們可以定義CustomStack<int>,CustomStack<double>或CustomStack<string>類型的棧,但有時我們必須限制泛型型別。這時我們需要使用類型約束,引入關鍵字----where。
C#中提供五種類型約束(類名,介面名,參考型別,實值型別和建構函式),我們可以通過使用不同的類型約束來限定泛型型別。
類型約束 |
約束描述 |
ClassName |
T只能是該類或繼承該類的子類 |
InterfaceName |
T只能是該介面或實行該介面的類型 |
struct |
T是任意實值型別 |
class |
T是任意參考型別(如classes, arrays, delegates, and interfaces等) |
new() |
T是包含公有無參構成函數的任意類型 |
表1類型約束
通過表1我們初步地瞭解不同類型約束之間的區別,現在讓我們通過具體的例子來看看它們使用範圍和區別。
假設我們定義了一個泛型結構體struct RefSample<T> where T : class,現在考考大家以下例子符合類約束的有(提示實值型別和參考型別區別):
A. RefSample<IDisposable>
B. RefSample<string>
C. RefSample<int[]>
D. RefSample<Guid>
E. RefSample<int>
激動人心的時刻又到了現在讓我們快快公布答案吧!答案是ABC,因為該泛型的約束是參考型別約束,如果大家還不清楚請看一下參考型別約束的定義。
通過類約束例子,我們舉一反三實值型別約束就是約束類型為實值型別。
OK接下來讓我們介紹建構函式約束。在介紹建構函式約束之前,讓我們回顧一下符合建構函式約束的條件:任意實值型別,任意非靜態非抽象無明確定義建構函式的類和任意非抽象有明確定義無參建構函式的類。這句話很彆扭,讓我們通過具體例子講解一下。
//// Define generic method//// Which has constructor type constraintspublic T CreateInstance<T>() where T : new() {return new T(); }CreateInstance<int>();CreateInstance<object>();CreateInstance<string>();
現在我們定義了一個泛型方法並且添加建構函式約束,調用CreateInstance<int>() 和 CreateInstance<object>() 成功,但調用CreateInstance<string>() 失敗,這由於String類沒有提供無參建構函式。也許有人會問:“我們很難判斷每種類型是否包含預設構無參造函數”,確確是這樣但我們可以記住的是C#規定實值型別提供預設無參建構函式。
前面的泛型例子都是只使用一種類型約束,但有時候我們要使用多種類型約束,這時我們就可以使用組合約束,在使用約束的時我們要注意約束的先後次序,約束次序如下:
圖5約束次序
通過我們發現ClassName,class和struct是主要約束,介面名是第二約束,最後是建構函式約束。OK現在我們對組合約束有了初步地瞭解,那麼讓我們通過以下例子加深理解。
A. class Sample<T> where T : class, struct
B. class Sample<T> where T : Stream, class
C. class Sample<T> where T : new(), Stream
D. class Sample<T> where T : IDisposable, Stream
E. class Sample<T> where T : XmlReader, IComparable, IComparable
F. class Sample<T,U> where T : struct where U : class, T
G. class Sample<T,U> where T : Stream, U : IDisposable
上面給出錯誤使用組合約束的例子(註:A~E單類型組合約束,FG多類型組合約束),請大家指出約束錯誤的原因,如果不清楚錯誤的原因大家回憶一下約束定義。
1.1.3 總結
本文注意介紹了C# 中泛型的基礎知識給出自己的總結,而且這不是C# 泛型的全部,如果大家在看完本文之後希望進一步學習泛型這才是我的目的。
泛型的優點:
- 編譯時間進行類型檢測,減少運行時異常InvalidCastException。
- 泛型使用強資料類型。如我們執行個體一個CustomStack<CustomStruct>對象objStack,通過調用Push()方法成功把CustomStruct壓入棧中,而objStack.Push(“Stack”)失敗。
- 實值型別無需進行boxing和unboxing操作。例如前面的泛型Push()和Pop()方法存放和取出實值型別時無需進行boxing和unboxing操作。
- 減少代碼量。如:我們定義一個泛型棧,就不用像前面例子那樣分別定義int和string類型的棧。
- 程式效能提高,因為無需類型轉換,從而減少類型檢測。
- 使用泛型減少記憶體消耗。因為無需進行boxing操作,不用重新分配堆空間。
- 代碼可讀性更強。
很多人喜歡將C# ,C++ 和Java中的泛型進行對比,而且也有很多相關的討論,在這裡我也給出自己的想法,我覺得C++ 的泛型功能的確比C# 和Java都要強大。