泛型(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;
Node current = head;
swapped = false;
while (current.next != null)
{
// Because we need to call this method, the SortedList
// class is constrained on IEnumerable<T>
if (current.Data.CompareTo(current.next.Data) > 0)
{
Node tmp = current.next;
current.next = current.next.next;
tmp.next = current;
if (previous == null)
{
head = tmp;
}
else
{
previous.next = tmp;
}
previous = tmp;
swapped = true;
}
else
{
previous = current;
current = current.next;
}
}// end while
} while (swapped);
}
}
// A simple class that implements IComparable<T>
// using itself as the type argument. This is a
// common design pattern in objects that are
// stored in generic lists.
public class Person : IComparable<Person>
{
string name;
int age;
public Person(string s, int i)
{
name = s;
age = i;
}
// This will cause list elements
// to be sorted on age values.
public int CompareTo(Person p)
{
return age - p.age;
}
public override string ToString()
{
return name + ":" + age;
}
// Must implement Equals.
public bool Equals(Person p)
{
return (this.age == p.age);
}
}
class Program
{
static void Main(string[] args)
{
//Declare and instantiate a new generic SortedList class.
//Person is the type argument.
SortedList<Person> list = new SortedList<Person>();
//Create name and age values to initialize Person objects.
string[] names = new string[]{"Franscoise", "Bill", "Li", "Sandra", "Gunnar", "Alok", "Hiroyuki", "Maria", "Alessandro", "Raul"};
int[] ages = new int[]{45, 19, 28, 23, 18, 9, 108, 72, 30, 35};
//Populate the list.
for (int x = 0; x < 10; x++)
{
list.AddHead(new Person(names[x], ages[x]));
}
//Print out unsorted list.
foreach (Person p in list)
{
Console.WriteLine(p.ToString());
}
//Sort the list.
list.BubbleSort();
//Print out sorted list.
foreach (Person p in list)
{
Console.WriteLine(p.ToString());
}
Console.WriteLine("Done");
}
}
可以在一個類型指定多個接口作為約束,如下:
class Stack<T> where T : IComparable<T>, IMyStack1<T>{}
一個接口可以定義多個類型參數,如下:
IDictionary<K,V>
接口和類的繼承規則相同:
//Okay.
IMyInterface : IBaseInterface<int>
//Okay.
IMyInterface<T> : IBaseInterface<T>
//Okay.
IMyInterface<T>: IBaseInterface<int>
//Error.
IMyInterface<T> : IBaseInterface2<T, U>
具體類可以實現封閉構造接口,如下:
class MyClass : IBaseInterface<string>
泛型類可以實現泛型接口或封閉構造接口,只要類的參數列表提供了接口需要的所有參數,如下:
//Okay.
class MyClass<T> : IBaseInterface<T>
//Okay.
class MyClass<T> : IBaseInterface<T, string>
泛型類、泛型結構,泛型接口都具有同樣方法重載的規則。詳細信息,請參見泛型方法。
七、泛型方法
泛型方法是聲名了類型參數的方法,如下:
void Swap<T>( ref T lhs, ref T rhs)
{
T temp;
temp = lhs;
lhs = rhs;
rhs = temp;
}
下面的示例代碼顯示了一個以int作為類型參數,來調用方法的例子:
int a = 1;
int b = 2;
//…
Swap<int>(a, b);
也可以忽略類型參數,編譯器會去推斷它。下面調用Swap的代碼與上面的例子等價:
Swap(a, b);
靜態方法和實例方法有着同樣的類型推斷規則。編譯器能夠根據傳入的方法參數來推斷類型參數;而無法單獨根據約束或返回值來判斷。因此類 型推斷對沒有參數的方法是無效的。類型推斷發生在編譯的時候,且在編譯器解析重載方法標志之前。編譯器對所有同名的泛型方法應用類型推斷邏輯。在決定 (resolution)重載的階段,編譯器只包含那些類型推斷成功的泛型類。更多信息,請參見C# 2.0規范,20.6.4類型參數推斷
在泛型方法中,非泛型方法能訪問所在類中的類型參數,如下:
class MyClass<T>
{
//…
void Swap (ref T lhs, ref T rhs){…}
}
[JX1] 定義一個泛型方法,和其所在的類具有相同的類型參數;試圖這樣做,編譯器會產生警告CS0693。
class MyList<T>
{
// CS0693
void MyMethod<T>{...}
}
class MyList<T>
{
//This is okay, but not common.
void SomeMethod<U>(){...}
}
使用約束可以在方法中使用更多的類型參數的特定方法。這個版本的Swap<T>稱為SwapIfGreater<T>,它只能使用實現了IComparable<T>的類型參數。
void SwapIfGreater<T>( ref T lhs, ref T rhs) where T: IComparable<T>
{
T temp;
if(lhs.CompareTo(rhs) > 0)
{
temp = lhs;
lhs = rhs;
rhs = temp;
}
}
泛型方法通過多個類型參數來重載。例如,下面的這些方法可以放在同一個類中:
void DoSomething(){}
void DoSomething<T>(){}
void DoSomething<T,U>(){}
八、泛型委托
無論是在類定義內還是類定義外,委托可以定義自己的類型參數。引用泛型委托的代碼可以指定類型參數來創建一個封閉構造類型,這和實例化泛型類或調用泛型方法一樣,如下例所示:
public delegate void MyDelegate<T>(T item);
public void Notify(int i){}
//...
MyDelegate<int> m = new MyDelegate<int>(Notify);
C#2.0版有個新特性稱為方法組轉換(method group conversion),具體代理和泛型代理類型都可以使用。用方法組轉換可以把上面一行寫做簡化語法:
MyDelegate<int> m = Notify;
在泛型類中定義的委托,可以與類的方法一樣地使用泛型類的類型參數。
class Stack<T>
{
T[] items;
int index
//...
public delegate void StackDelegate(T[] items);
}
引用委托的代碼必須要指定所在類的類型參數,如下:
Stack<float> s = new Stack<float>();
Stack<float>.StackDelegate myDelegate = StackNotify;
泛型委托在定義基於典型設計模式的事件時特別有用。因為sender[JX2] ,而再也不用與Object相互轉換。
public void StackEventHandler<T,U>(T sender, U eventArgs);
class Stack<T>
{
//…
public class StackEventArgs : EventArgs{...}
public event StackEventHandler<Stack<T>, StackEventArgs> stackEvent;
protected virtual void OnStackChanged(StackEventArgs a)
{
stackEvent(this, a);
}
}
class MyClass
{
public static void HandleStackChange<T>(Stack<T> stack, StackEventArgs args){...};
}
Stack<double> s = new Stack<double>();
MyClass mc = new MyClass();
s.StackEventHandler += mc.HandleStackChange;
九、泛型代碼中的
default
關鍵字
在泛型類和泛型方法中會出現的一個問題是,如何把缺省值賦給參數化類型,此時無法預先知道以下兩點:
l T將是值類型還是引用類型
l 如果T是值類型,那么T將是數值還是結構
對於一個參數化類型T的變量t,僅當T是引用類型時,t = null語句才是合法的; t = 0只對數值的有效,而對結構則不行。這個問題的解決辦法是用default關鍵字,它對引用類型返回空,對值類型的數值型返回零。而對於結構,它將返回結 構每個成員,並根據成員是值類型還是引用類型,返回零或空。下面MyList<T>類的例子顯示了如何使用default關鍵字。更多信息, 請參見泛型概述。
public class MyList<T>
{
//...
public T GetNext()
{
T temp = default(T);
if (current != null)
{
temp = current.Data;
current = current.Next;
}
return temp;
}
}