定義:
所謂泛型,即通過參數化類型實現在同一份代碼上操作多種類型的數據,泛型編程是一種范式的轉化(在這裡體現為類型的晚綁定),他利用參數化類型,將類型抽象化,從而實現代碼的靈活復用,精簡代碼。
泛型的好處:
a.減少了對對象進行裝箱和拆箱所導致的性能成本,提高了效率。
b.賦予了代碼更強的類型安全。
c.實現了更為靈活的代碼復用。
注:1.NET參數化類型不是編譯(JIT編譯)時被實例化,而是運行時被實例化。
2.由微軟在產品文檔中提出建議,所有的泛型參數名稱都以T開頭,這是作為一種編碼的通用規范。
在定義泛型時,可以對客戶端代碼在實例化類時用於類型參數的類型施加一些限制,如果客戶端代碼嘗試使用某個約束所不允許的類型來實例化類,則會產生編譯錯誤,這些限制稱為約束,約束是使用where關鍵字實現的。
每個泛型參數至少擁有一個主約束,泛型的主約束是指指定泛型參數必須是或者繼承自某個引用類型。每個泛型參數可以具有多個次約束,次約束和主約束的語法基本相同,但它規定的是某個泛型參數必須實現所有次約束指定的接口。
下面列出了五種類型的約束:
T:struct 類型參數必須為值類型,可以指定除 Nullable 以外的任何值類型。
T:class 類型參數必須為引用類型,包括類、接口、委托、和數組。
T:new() 類型參數必須具有無參公共構造函數,當與其他約束一起使用時,new() 約束必須最後指定。
T:<基類名> 類型參數必須為指定的基類或繼承自該基類的子類。
T:<接口名稱> 類型參數必須是指定的接口或實現指定的接口。可以指定多個接口約束。約束接口也可以是泛型的。
T:U 為 T 提供的類型參數必須是為 U 提供的參數或派生自為 U 提供的參數。這稱為裸類型約束.
泛型的編譯和運行機制
C#泛型能力由CLR在運行時支持,區別於C++編譯時模板的機制,和java編譯時的“搽拭法”,這事得泛型能力可以在各個支持CLR語言之間進行無縫的互操作。C#泛型代碼在編譯為IL代碼和元數據時,采用特殊的占位符來表示泛型實例,並用專有的IL指令來支持泛型操作,而真正的泛型實例化工作發生在JIT編譯時。
這裡我們對三種語言對泛型的支持做一個對比:
C++的模板機制:C++在編譯時會根據每一個傳入的類型參數創建一份基於特定類型的類型碼,因此如果有多個類型參數傳入,編譯時就會生成多份相似的類型碼,容易導致代碼膨脹。因此C++的泛型只是實現了在源代碼層面的復用,並沒有實現IL層面的代碼復用。
C#:第一輪編譯時,編譯器只會為泛型類型產生一個“泛型版”的IL代碼和元數據,並不進行泛型類型的實例化,T在中間只是充當占位符,這一點可以通過下面展示的一份泛型的IL代碼來證明。只有JIT編譯時CLR才會針對不同的類型產生不同的類型碼,在類型碼產生的過程中,CLR進行了許多的優化:1.CLR為所有引用類型的類型參數產生一份共同的一份類型碼,所有的引用類型共用這一份類型碼。2.對於每一個不同的值類型,CLR將為其產生一份獨立的類型碼。另外,C#泛型類型攜帶有豐富的元數據,因此C#的泛型類型可以應用於強大的反射技術。
java:java在進行第一階段的編譯時,將泛型類型用Object類型進行替換,因此類型參數實例化的時候需要進行大量的裝箱和拆箱工作,但是這些工作並不需要我們去做,編譯器會自動的進行這些工作,性能成本比較高。本質上講並沒有實現真正意義上的泛型,是編譯器的一種欺騙行為。
下面通過具體的例子來一點一點的深入泛型。
我們來實現一個最簡單的冒泡排序(Bubble Sort)算法,如果你沒有使用泛型的經驗,我猜測你可能會毫不猶豫地寫出下面的代碼來,因為這是大學教程的標准實現:
public class Sort{
public void BubbleSort(int[] array) {
for (int i = 0; i <= array.Length - 2; i++) {
for (int j = array.Length - 1; j >= 1; j--){
if (array[j] < array[j - 1]) {
int temp = array[j];
array[j] = array[j - 1];
array[j - 1] = temp;
}
}
}
}
}
後來我們需要對一個byte類型的數組進行排序,而上面排序的方法只能對int型的數組進行排序,因此我們不得不重寫代碼:
public class Sort{
public void BubbleSort(byte[] array) {
for (int i = 0; i <= array.Length - 2; i++) {
for (int j = array.Length - 1; j >= 1; j--){
if (array[j] < array[j - 1]) {
byte temp = array[j];
array[j] = array[j - 1];
array[j - 1] = temp;
}
}
}
}
}
現在我們將int[]和byte[]用占位符來替代,形成一種通用的代碼:
public class Sort{
public void BubbleSort(T[] array) {
for (int i = 0; i <= array.Length - 2; i++) {
for (int j = array.Length - 1; j >= 1; j--){
if (array[j] < array[j - 1]) {
T temp = array[j];
array[j] = array[j - 1];
array[j - 1] = temp;
}
}
}
}
}
但是我們又發現了一個問題:當我們定義一個類,而這個類需要引用它本身以外的其他類型時,如何將這個類型參數傳進來了,此時就需要使用一種特殊的語法來傳遞這個T占位符,我們在類名稱的後面加了一個尖括號,使用這個尖括號來傳遞我們的占位符,也就是類型參數。
public class Sort<T>{
public void BubbleSort(T[] array) {
for (int i = 0; i <= array.Length - 2; i++) {
for (int j = array.Length - 1; j >= 1; j--){
if (array[j] < array[j - 1]) {
T temp = array[j];
array[j] = array[j - 1];
array[j - 1] = temp;
}
}
}
}
}
使用的時候我們就可以這樣使用:
public class Test{
public static void Main(){
Sort<int> sorter = new Sort<int>();
int[] array = { 8, 1, 4, 7, 3 };
sorter.BubbleSort(array);
}
}
上面所講述的一切都是一個泛型的典型應用,可以看到,通過使用泛型,我們極大地減少了重復代碼,使我們的程序更加清爽,泛型類就類似於一個模板,可以在需要時為這個模板傳入任何我們需要的類型。
下面我們來談一下泛型約束。
實際上,如果你運行一下上面的代碼,發現他們無法通過編譯,為什麼了,就是因為有了T的存在,T是晚綁定的,因此在編譯時編譯器無法得知T的實例是采用什麼樣的標准來進行大小的比較的,下面我們舉例說明:
假如我們有一個自定義的類Book,它定義了書,它包含兩個私有字段_id和_title,兩個外部屬性:ID和Title,以及兩個構造器
public class Book
{
private int _id;
private string _title;
public Book(){ }
public Book(int id,string title)
{
this._id=id;
this._title=title
}
public int ID
{
get{return _id;}
set{_id=value;}
}
public string Title
{
get{return _title;}
set{_title=value;}
}
}
現在我們創建一個Book型的數組,然後用Sort類中的方法對其進行排序:
class Test{
static void Main()
{
Book[] bookArray=new Book[2];
Book book1=new Book(1,"guowenhui");
Book book2=new Book(2,"dongyaguang");
bookArray[0]=book1;
bookArray[1]=book2;
Sort<Book> sort=new Sort<Book>;
Sort.BubbleSort(bookArray);
foreach (Book b in bookArray) {
Console.WriteLine("Id:{0}", b.Id);
Console.WriteLine("Title:{0}\n", b.Title);
}
}
}
可能你覺得這樣很好,基本沒什麼問題,但是我們來看看BubbleSort()方法的實現,我截取關鍵的一段:
for (int j = array.Length - 1; j >= 1; j--){
if (array[j] < array[j - 1]) {
T temp = array[j];
array[j] = array[j - 1];
array[j - 1] = temp;
}
}
大家會看到if語句裡面會對數組裡面的兩個元素進行比較,那麼問題就出在這兒,以前當類型為int型的時候,我們直接這樣進行比較,無可厚非,但是現在不同了,我們的類型是Book,那麼我要問,book1和book2到底誰大了,有的人說book1大,有的人說book2大,這裡就涉及到一個判斷依據的問題。那麼如何來實現這種比較了,答案是:讓需要進行比較的類實現IComparable接口。也就是說只有實現了IComparable接口的類型才能作為類型參數被傳入,即我們需要對傳入參數的類型進行一些約束,這就是我們要講的泛型約束,在本例中我們實現的是接口約束。
接下來我們就讓Book類來實現IComparable接口,即在類的內部定義一個比較的標准,我們這裡采用的標准是比較ID:
public class Book : IComparable
{
public int CompareTo(object obj) //實現接口
{
Book book2=(Book)obj;
return this.ID.CompareTo(book2.ID);
}
private int _id;
private string _title;
public Book(){ }
public Book(int id,string title)
{
this._id=id;
this._title=title;
}
public int ID
{
get{return _id;}
set{_id=value;}
}
public string Title
{
get{return _title;}
set{_title=value;}
}
}
現在我們應該可以進行比較了吧,還不行,因為Sort類是一個泛型類,JIT編譯時編譯器對於傳入該類的類型參數一無所知(類型的晚綁定),明確的說需要等到運行時才能確定參數,也不會做任何猜想,雖然我們知道Book類實現了
IComparable接口,但編譯器並不知道,因此我們必須Sort<T>類(即告訴JIT編譯器),它所接受的類型參數必須實現了IComparable接口,這便是泛型約束,下面我們來對泛型類Sort<T>的傳入參數進行約束,同時我們對比較大小的方法進行一些修改。
public class Sort<T> where T : IComparable
{
public void BubbleSort(T[] array) {
for (int i = 0; i <= array.Length - 2; i++) {
for (int j = array.Length - 1; j >= 1; j--){
if (array[j].CompareTo(array[j-1])<0) {
T temp = array[j];
array[j] = array[j - 1];
array[j - 1] = temp;
}
}
}
}
}
此時我們再次運行下面定義的代碼
class Test{
static void Main()
{
Book[] bookArray=new Book[2];
Book book1=new Book(1,"guowenhui");
Book book2=new Book(2,"dongyaguang");
bookArray[0]=book1;
bookArray[1]=book2;
Sort<Book> sort=new Sort<Book>();
Sort.BubbleSort(bookArray);
foreach (Book b in bookArray) {
Console.WriteLine("Id:{0}", b.ID);
Console.WriteLine("Title:{0}\n", b.Title);
}
Console.ReadLine();
}
}
會得到結果:
ID:1
Title:guowenhui
ID:2
Title:dongyaguang
下面是完整的代碼:
View Code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ConsoleApplication1
{
public class Book : IComparable
{
public int CompareTo(object obj) //實現接口
{
Book book2 = (Book)obj;
return this.ID.CompareTo(book2.ID);
}
private int _id;
private string _title;
public Book() { }
public Book(int id, string title)
{
this._id=id;
this._title = title;
}
public int ID
{
get { return _id; }
set { _id = value; }
}
public string Title
{
get { return _title; }
set { _title = value; }
}
}
public class Sort<T> where T : IComparable
{
public void BubbleSort(T[] array)
{
for (int i = 0; i <= array.Length - 2; i++)
{
for (int j = array.Length - 1; j >= 1; j--)
{
if (array[j].CompareTo(array[j - 1]) < 0)
{
T temp = array[j];
array[j] = array[j - 1];
array[j - 1] = temp;
}
}
}
}
}
class Test
{
static void Main()
{
Book[] bookArray=new Book[2];
Book book1=new Book(1,"guowenhui");
Book book2=new Book(2,"dongyaguang");
bookArray[0]=book1;
bookArray[1]=book2;
Sort<Book> sort = new Sort<Book>();
sort.BubbleSort(bookArray);
foreach (Book b in bookArray)
{
Console.WriteLine("Id:{0}", b.ID);
Console.WriteLine("Title:{0}\n", b.Title);
}
Console.ReadLine();
}
}
}
泛型接口
沒有泛型接口,每次試圖使用一個非泛型接口(如IComparable)來操縱一個值類型時,都會進行裝箱,而且會丟失編譯時的類型安全性。這會嚴重限制泛型類型的應用。所以,CLR提供了對泛型接口的支持。一個引用類型或值類型為了實現一個泛型接口,可以具體指定類型實參;另外,一個類型也可以保持類型實參的未指定狀態來實現一個泛型接口。來看一些例子:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; namespace ConsoleApplication1 { interface ITest<T, V> //泛型接口的定義 where T : class where V : struct { void Print(T t, V v); } class TestA //自定義引用類型 { private string _name; public string Name { set { _name = value; } get { return _name; } } } struct TestB //自定義值類型 { private int _age; public int Age { get { return _age; } set { _age = value; } } } class TestC : ITest<TestA,TestB> //繼承並實現接口 { public void Print(TestA A, TestB B) { Console.WriteLine(A.Name + " is " + B.Age); } } class Program { static void Main() { TestA testA = new TestA(); testA.Name = "guowenhui"; TestB testB = new TestB(); testB.Age = 21; TestC testC = new TestC(); testC.Print(testA, testB); Console.ReadLine(); } } }