泛型(generic)是C#語言2.0和通用語言運行時(CLR)的一個新特性。泛型為.Net框架引入了類型參數(type parameters)的概念。類型參數使得設計類和方法時,不必確定一個或多個具體參數,其的具體參數可延遲到客戶代碼中聲明、實現。這意味著使用泛型的類型參數T,寫一個類MyList<T>,客戶代碼可以這樣調用:MyList<int>, MyList<string>或 MyList<MyClass>。這避免了運行時類型轉換或裝箱操作的代價和風險。
目錄
C#中的泛型. 1
一、泛型概述. 2
二、泛型的優點. 5
三、泛型類型參數. 7
四、類型參數的約束. 8
五、泛型類. 11
六、泛型接口. 13
七、泛型方法. 19
八、泛型委托. 21
九、泛型代碼中的default關鍵字. 23
十、C++模板和C#泛型的區別. 24
十一、運行時中的泛型. 25
十二、基礎類庫中的泛型. 27
一、泛型概述
泛型類和泛型方法兼復用性、類型安全和高效率於一身,是與之對應的非泛型的類和方法所不及。泛型廣泛用於容器(collections)和對容器操作的方法中。.NET框架2.0的類庫提供一個新的命名空間System.Collections.Generic,其中包含了一些新的基於泛型的容器類。要查找新的泛型容器類(collection classes)的示例代碼,請參見基礎類庫中的泛型。當然,你也可以創建自己的泛型類和方法,以提供你自己的泛化的方案和設計模式,這是類型安全且高效的。下面的示例代碼以一個簡單的泛型鏈表類作為示范。(多數情況下,推薦使用由.Net框架類庫提供的List<T>類,而不是創建自己的表。)類型參數T在多處使用,具體類型通常在這些地方來指明表中元素的類型。類型參數T有以下幾種用法:
l 在AddHead方法中,作為方法參數的類型。
l 在公共方法GetNext中,以及嵌套類Node的 Data屬性中作為返回值的類型。
l 在嵌套類中,作為私有成員data的類型。
注意一點,T對嵌套的類Node也是有效的。當用一個具體類來實現MyList<T>時——如MyList<int>——每個出現過的T都要用int代替。
using System;
using System.Collections.Generic;
public class MyList<T> //type parameter T in angle brackets
{
private Node head;
// The nested type is also generic on T.
private class Node
{
private Node next;
//T as private member data type:
private T data;
//T used in non-generic constructor:
public Node(T t)
{
next = null;
data = t;
}
public Node Next
{
get { return next; }
set { next = value; }
}
//T as return type of property:
public T Data
{
get { return data; }
set { data = value; }
}
}
public MyList()
{
head = null;
}
//T as method parameter type:
public void AddHead(T t)
{
Node n = new Node(t);
n.Next = head;
head = n;
}
public IEnumerator<T> GetEnumerator()
{
Node current = head;
while (current != null)
{
yIEld return current.Data;
current = current.Next;
}
}
}
下面的示例代碼演示了客戶代碼如何使用泛型類MyList<T>,來創建一個整數表。通過簡單地改變參數的類型,很容易改寫下面的代碼,以創建字符串或其他自定義類型的表。
class Program
{
static void Main(string[] args)
{
//int is the type argument.
MyList<int> list = new MyList<int>();
for (int x = 0; x < 10; x++)
list.AddHead(x);
foreach (int i in list)
{
Console.WriteLine(i);
}
Console.WriteLine("Done");
}
}
二、泛型的優點
針對早期版本的通用語言運行時和C#語言的局限,泛型提供了一個解決方案。以前類型的泛化(generalization)是靠類型與全局基類System.Object的相互轉換來實現。.Net框架基礎類庫的ArrayList容器類,就是這種局限的一個例子。ArrayList是一個很方便的容器類,使用中無需更改就可以存儲任何引用類型或值類型。
//The .Net Framework 1.1 way of creating a list
ArrayList list1 = new ArrayList();
list1.Add(3);
list1.Add(105);
//...
ArrayList list2 = new ArrayList();
list2.Add(“It is raining in Redmond.”);
list2.Add("It is snowing in the Mountains.");
//...
但是這種便利是有代價的,這需要把任何一個加入ArrayList的引用類型或值類型都隱式地向上轉換成System.Object。如果這些元素是值類型,那麼當加入到列表中時,它們必須被裝箱;當重新取回它們時,要拆箱。類型轉換和裝箱、拆箱的操作都降低了性能;在必須迭代(iterate)大容器的情況下,裝箱和拆箱的影響可能十分顯著。
另一個局限是缺乏編譯時的類型檢查,當一個ArrayList把任何類型都轉換為Object,就無法在編譯時預防客戶代碼類似這樣的操作:
ArrayList list = new ArrayList();
//Okay.
list.Add(3);
//Okay, but did you really want to do this?
list.Add(.“It is raining in Redmond.”);
int t = 0;
//This causes an InvalidCastException to be returned.
foreach(int x in list)
{
t += x;
}
雖然這樣完全合法,並且有時是有意這樣創建一個包含不同類型元素的容器,但是把string和int變量放在一個ArrayList中,幾乎是在制造錯誤,而這個錯誤直到運行的時候才會被發現。
在1.0版和1.1版的C#語言中,你只有通過編寫自己的特定類型容器,才能避免.Net框架類庫的容器類中泛化代碼(generalized code)的危險。當然,因為這樣的類無法被其他的數據類型復用,也就失去泛型的優點,你必須為每個需要存儲的類型重寫該類。
ArrayList和其他相似的類真正需要的是一種途徑,能讓客戶代碼在實例化之前指定所需的特定數據類型。這樣就不需要向上類型轉換為Object,而且編譯器可以同時進行類型檢查。換句話說,ArrayList需要一個類型參數。這正是泛型所提供的。在System.Collections.Generic命名空間中的泛型List<T>容器裡,同樣是把元素加入容器的操作,類似這樣:
The .Net Framework 2.0 way of creating a list
List<int> list1 = new List<int>();
//No boxing, no casting:
list1.Add(3);
//Compile-time error:
list1.Add("It is raining in Redmond.");
與ArrayList相比,在客戶代碼中唯一增加的List<T>語法是聲明和實例化中的類型參數。代碼略微復雜的回報是,你創建的表不僅比ArrayList更安全,而且明顯地更加快速,尤其當表中的元素是值類型的時候。
三、泛型類型參數
在泛型類型或泛型方法的定義中,類型參數是一個占位符(placeholder),通常為一個大寫字母,如T。在客戶代碼聲明、實例化該類型的變量時,把T替換為客戶代碼所指定的數據類型。泛型類,如泛型概述中給出的MyList<T>類,不能用作as-is,原因在於它不是一個真正的類型,而更像是一個類型的藍圖。要使用MyList<T>,客戶代碼必須在尖括號內指定一個類型參數,來聲明並實例化一個已構造類型(constructed type)。這個特定類的類型參數可以是編譯器識別的任何類型。可以創建任意數量的已構造類型實例,每個使用不同的類型參數,如下:
MyList<MyClass> list1 = new MyList<MyClass>();
MyList<float> list2 = new MyList<float>();
MyList<SomeStruct> list3 = new MyList<SomeStruct>();
在這些MyList<T>的實例中,類中出現的每個T都將在運行的時候被類型參數所取代。依靠這樣的替換,我們僅用定義類的代碼,就創建了三個獨立的類型安全且高效的對象。有關CLR執行替換的詳細信息,請參見運行時中的泛型。
四、類型參數的約束
若要檢查表中的一個元素,以確定它是否合法或是否可以與其他元素相比較,那麼編譯器必須保證:客戶代碼中可能出現的所有類型參數,都要支持所需調用的操作或方法。這種保證是通過在泛型類的定義中,應用一個或多個約束而得到的。一個約束類型是一種基類約束,它通知編譯器,只有這個類型的對象或從這個類型派生的對象,可被用作類型參數。一旦編譯器得到這樣的保證,它就允許在泛型類中調用這個類型的方法。上下文關鍵字where用以實現約束。下面的示例代碼說明了應用基類約束,為MyList<T>類增加功能。
public class Employee
{
public class Employee
{
private string name;
private int id;
public Employee(string s, int i)
{
name = s;
id = i;
}
public string Name
{
get { return name; }
set { name = value; }
}
public int ID
{
get { return id; }
set { id = value; }
}
}
}
class MyList<T> where T: Employee
{
//Rest of class as before.
public T FindFirstOccurrence(string s)
{
T t = null;
Reset();
while (HasItems())
{
if (current != null)
{
//The constraint enables this:
if (current.Data.Name == s)
{
t = current.Data;
break;
}
else
{
current = current.Next;
}
} //end if
} // end while
return t;
}
}
約束使得泛型類能夠使用Employee.Name屬性,因為所有為類型T的元素,都是一個Employee對象或是一個繼承自Employee的對象。
同一個類型參數可應用多個約束。約束自身也可以是泛型類,如下:
class MyList<T> where T: Employee, IEmployee, IComparable<T>, new()
{…}
下表列出了五類約束:
約束
描述
where T: struct
類型參數必須為值類型。
where T : class
類型參數必須為類型。
where T : new()
類型參數必須有一個公有、無參的構造函數。當於其它約束聯合使用時,new()約束必須放在最後。
where T : <base class name>
類型參數必須是指定的基類型或是派生自指定的基類型。
where T : <interface name>
類型參數必須是指定的接口或是指定接口的實現。可以指定多個接口約束。接口約束也可以是泛型的。
類型參數的約束,增加了可調用的操作和方法的數量。這些操作和方法受約束類型及其派生層次中的類型的支持。因此,設計泛型類或方法時,如果對泛型成員執行任何賦值以外的操作,或者是調用System.Object中所沒有的方法,就需要在類型參數上使用約束。
無限制類型參數的一般用法
沒有約束的類型參數,如公有類MyClass<T>{...}中的T, 被稱為無限制類型參數(unbounded type parameters)。無限制類型參數有以下規則:
l 不能使用運算符 != 和 == ,因為無法保證具體的類型參數能夠支持這些運算符。
l 它們可以與System.Object相互轉換,也可顯式地轉換成任何接口類型。
l 可以與null比較。如果一個無限制類型參數與null比較,當此類型參數為值類型時,比較的結果總為false。
無類型約束
當約束是一個泛型類型參數時,它就叫無類型約束(Naked type constraints)。當一個有類型參數成員方法,要把它的參數約束為其所在類的類型參數時,無類型約束很有用。如下例所示:
class List<T>
{
//...
void Add<U>(List<U> items) where U:T {…}
}
在上面的示例中, Add方法的上下文中的T,就是一個無類型約束;而List類的上下文中的T,則是一個無限制類型參數。
無類型約束也可以用在泛型類的定義中。注意,無類型約束一定也要和其它類型參數一起在尖括號中聲明:
//naked type constraint
public class MyClass<T,U,V> where T : V
因為編譯器只認為無類型約束是從System.Object繼承而來,所以帶有無類型約束的泛型類的用途十分有限。當你希望強制兩個類型參數具有繼承關系時,可對泛型類使用無類型約束。
五、泛型類
泛型類封裝了不針對任何特定數據類型的操作。泛型類常用於容器類,如鏈表、哈希表、棧、隊列、樹等等。這些類中的操作,如對容器添加、刪除元素,不論所存儲的數據是何種類型,都執行幾乎同樣的操作。
對大多數情況,推薦使用.Net框架2.0類庫中所提供的容器類。有關使用這些類的詳細信息,請參見基礎類庫中的泛型。
通常,從一個已有的具體類來創建泛型類,並每次把一個類型改為類型參數,直至達到一般性和可用性的最佳平衡。當創建你自己的泛型類時,需要重點考慮的事項有:
l 哪些類型應泛化為類型參數。一般的規律是,用參數表示的類型越多,代碼的靈活性和復用性也就越大。過多的泛化會導致代碼難以被其它的開發人員理解。
l 如果有約束,那麼類型參數需要什麼樣約束。一個良好的習慣是,盡可能使用最大的約束,同時保證可以處理所有需要處理的類型。例如,如果你知道你的泛型類只打算使用引用類型,那麼就應用這個類的約束。這樣可以防止無意中使用值類型,同時可以對T使用as運算符,並且檢查空引用。
l 把泛型行為放在基類中還是子類中。泛型類可以做基類。同樣非泛型類的設計中也應考慮這一點。泛型基類的繼承規則 。
l 是否實現一個或多個泛型接口。例如,要設計一個在基於泛型的容器中創建元素的類,可能需要實現類似IComparable<T>的接口,其中T是該類的參數。
泛型概述中有一個簡單泛型類的例子。
類型參數和約束的規則對於泛型類的行為(behavior)有一些潛在的影響,——尤其是對於繼承和成員可訪問性。在說明這個問題前,理解一些術語十分重要。對於一個泛型類Node<T>,客戶代碼既可以通過指定一個類型參數來創建一個封閉構造類型(Node<int>),也可以保留類型參數未指定,例如指定一個泛型基類來創建開放構造類型(Node<T>)。泛型類可以繼承自具體類、封閉構造類型或開放構造類型:
// concrete type
class Node<T> : BaseNode
//closed constructed type
class Node<T> : BaseNode<int>
//open constructed type
class Node<T> : BaseNode<T>
非泛型的具體類可以繼承自封閉構造基類,但不能繼承自開放構造基類。這是因為客戶代碼無法提供基類所需的類型參數。
//No error.
class Node : BaseNode<int>
//Generates an error.
class Node : BaseNode<T>
泛型的具體類可以繼承自開放構造類型。除了與子類共用的類型參數外,必須為所有的類型參數指定類型,如下代碼所示:
//Generates an error.
class Node<T> : BaseNode<T, U> {…}
//Okay.
class Node<T> : BaseNode<T, int>{…}
繼承自開放結構類型的泛型類,必須指定:
Generic classes that inherit from open constructed types must specify must specify constraints that are a superset of, or imply, the constraints on the base type:
class NodeItem<T> where T : IComparable<T>, new() {…}
class MyNodeItem<T> : NodeItem<T> where T : IComparable<T> , new(){…}
泛型類型可以使用多種類型參數和約束,如下:
class KeyType<K,V>{…}
class SuperKeyType<K,V,U> where U : IComparable<U>, where V : new(){…}
開放結構和封閉構造類型型可以用作方法的參數:
void Swap<T>(List<T> list1, List<T> list2){…}
void Swap(List<int> list1, List<int> list2){…}
六、泛型接口
不論是為泛型容器類,還是表示容器中元素的泛型類,定義接口是很有用的。把泛型接口與泛型類結合使用是更好的用法,比如用IComparable<T>而非IComparable,以避免值類型上的裝箱和拆箱操作。.Net框架2.0類庫定義了幾個新的泛型接口,以配合System.Collections.Generic中新容器類的使用。
當一個接口被指定為類型參數的約束時,只有實現該接口的類型可被用作類型參數。下面的示例代碼顯示了一個從MyList<T>派生的SortedList<T>類。更多信息,請參見泛型概述。SortedList<T>增加了約束where T : IComparable<T>。
這使得SortedList<T>中的BubbleSort方法可以使用表中的元素的IComparable<T>.CompareTo方法。在這個例子中,表中的元素是簡單類——實現IComparable<Person>的Person類。
using System;
using System.Collections.Generic;
//Type parameter T in angle brackets.
public class MyList<T>
{
protected Node head;
protected Node current = null;
// Nested type is also generic on T
protected class Node
{
public Node next;
//T as private member datatype.
private T data;
//T used in non-generic constructor.
public Node(T t)
{
next = null;
data = t;
}
public Node Next
{
get { return next; }
set { next = value; }
}
//T as return type of property.
public T Data
{
get { return data; }
set { data = value; }
}
}
public MyList()
{
head = null;
}
//T as method parameter type.
public void AddHead(T t)
{
Node n = new Node(t);
n.Next = head;
head = n;
}
// Implement IEnumerator<T> to enable foreach
// iteration of our list. Note that in C# 2.0
// you are not required to implment Current and
// GetNext. The compiler does that for you.
public IEnumerator<T> GetEnumerator()
{
Node current = head;
while (current != null)
{
yIEld return current.Data;
current = current.Next;
}
}
}
public class SortedList<T> : MyList<T> where T : IComparable<T>
{
// A simple, unoptimized sort algorithm that
// orders list elements from lowest to highest:
public void BubbleSort()
{
if (null == head || null == head.Next)
return;
bool swapped;
do
{
Node previous = null;
arameters.
· C++ allows code that might not be valid for all type parameters in the template, which is then checked for the specific type used as the type parameter. C# requires code in a class to be written in such a way that it will work with any type that satisfIEs the constraints. For example, in C++ it is possible to write a function that uses the arithmetic operators + and - on objects of the type parameter, which will produce an error at the time of instantiation of the template with a type that does not support these Operators. C# disallows this; the only language constructs allowed are those that can be deduced from the constraints.
十一、運行時中的泛型
Specialized generic types are created once for each unique value type used as a parameter.
當泛型類或泛型方法被編譯為微軟中間語言(MSIL)後,它所包含的元數據定義了它的類型參數。根據所給的類型參數是值類型還是引用類型,對泛型類型所用的MSIL也是不同的。
當第一次以值類型作為參數來構造一個泛型類型,運行時用所提供的參數或在MSIL中適當位置被替換的參數,來創建一個專用的泛型類型。運行時會為每個唯一的作為參數的值類型創建一個的專用泛型類型。[JX3]
例如,假設你的程序代碼聲名一個由整型構成的棧,如:
Stack<int> stack;
此時,運行時用整型恰當地替換了它的類型參數,生成一個專用版本的棧。此後,程序代碼再用到整型棧時,運行時復用已創建的專用的棧。下面的例子創建了兩個整型棧的實例,它們共用一個Stack<int>代碼實例:
Stack<int> stackOne = new Stack<int>();
Stack<int> stackTwo = new Stack<int>();
然而,如果由另一種值類型——如長整型或用戶自定義的結構——作為參數,在代碼的其他地方創建另一個棧,那麼運行時會生成另一個版本的泛型類型。這次是把長整型替換到MSIL中的適當的位置。由於每個專用泛型類原本就包含值類型,因此不需要再轉換。
對於引用類型,泛型的工作略有不同。當第一次用任何引用類型構造泛型類時,運行時在MSIL中創建一個專用泛型類,其中的參數被對象引用所替換。之後,每當用一個引用類型作為參數來實例化一個已構造類型時,就忽略其類型,運行時復用先前創建的專用版本的泛型類。這可能是由於所有的引用的大小都相同。
例如,假如你有兩個引用類型,一個Customer類和一個Order類;進一步假設你創建了一個Customer的棧:
Stack<Customer> customers;
此時,運行時生成一個專用版本的棧,用於稍後存儲對象的引用,而不是存儲數據。假如下一行代碼創建了一個另一種引用類型的棧,名為Order:
Stack<Order> orders = new Stack<Order>();
和值類型不同,運行時並沒有為Order類型創建另一個棧的專用版本。相反,運行時創建了一個專用版本棧實例,並且變量orders指向這個實例。如果之後是一行創建Customer類型的棧的代碼:
customers = new Stack<Customer>();
和之前以Order類型創建的棧一樣,創建了專用棧的另一個實例,並且其中所包含的指針指向一塊大小與Customer類一致的內存。由於不同程序間引用類型的數量差別很大,而編譯器只為引用類型的泛型類創建一個專用類,因此C#對泛型的實現極大地降低了代碼膨脹。
此外,當用類型參數實現一個泛型C#類時,想知道它是指類型還是引用類型,可以在運行時通過反射確定它的真實類型和它的類型參數。
十二、基礎類庫中的泛型
2.0版的.NET框架類庫提供了一個新的命名空間,System.Collections.Generic,其中包含了一些已經可以使用的泛型容器類和相關的接口。和早期版本的.Net框架提供的非泛型容器類相比,這些類和接口更高效且是類型安全的。在設計、實現自定義的容器類之前,請你考慮是否使用或繼承所列出類中的一個。
下面的表格列出了新的泛型類和接口,旁邊是對應的非泛型類和接口。在一些地方要特別注意,如List<T>和Dictionary<T>,新泛型類的行為(behavior)與它們所替換的非泛型類有些不同,也不完全兼容。更詳細的內容,請參考System.Collections.Generic的文檔
泛型類或接口
描述
對應的非泛型類型
Collection<T>
ICollection<T>
為泛型容器提供基類
CollectionBase
ICollection
Comparer<T>
IComparer<T>
IComparable<T>
比較兩個相同泛型類型的對象是否相等、可排序。
Comparer
IComparer
IComparable
Dictionary<K, V>
IDictionary<K,V>
表示用鍵組織的鍵/值對集合。
Hashtable
IDictionary
Dictionary<K, V>.KeyCollection
表示Dictionary<K, V>中鍵的集合。
None.
Dictionary<K, V>.ValueCollection
表示Dictionary<K, V>中值的集合。
None.
IEnumerable<T>
IEnumerator<T>
表示可以使用foreach 迭代的集合。
IEnumerable
IEnumerator
KeyedCollection<T, U>
表示有鍵值的集合。
KeyedCollection
LinkedList<T>
表示雙向鏈表。
None.
LinkedListNode<T>
表示LinkedList<T>中的節點。
None.
List<T>
IList<T>
使用大小可按需動態增加的數組實現 IList 接口
ArrayList
IList
Queue<T>
表示對象的先進先出集合。
Queue
ReadOnlyCollection<T>
為泛型只讀容器提供基類。
ReadOnlyCollectionBase
SortedDictionary<K, V>
表示鍵/值對的集合,這些鍵和值按鍵排序並可按照鍵訪問,實現IComparer<T>接口。
SortedList
Stack<T>
表示對象的簡單的後進先出集合。
Stack
--------------------------------------------------------------------------------