generics:泛型
type-safe:類型安全
collection: 集合
compiler:編譯器
run time:程序運行時
object: 對象
.NET library:.Net類庫
value type: 值類型
box: 裝箱
unbox: 拆箱
implicity: 隱式
explicity: 顯式
linked list: 線性鏈表
node: 結點
indexer: 索引器html
簡介
Visual C# 2.0 的一個最受期待的(或許也是最讓人畏懼)的一個特性就是對於泛型的支持。這篇文章將告訴你泛型用來解決什麼樣的問題,以及如何使用它們來提升你的代碼質量,還有你沒必要恐懼泛型的緣由。node
泛型是什麼?
不少人以爲泛型很難理解。我相信這是由於他們一般在瞭解泛型是用來解決什麼問題以前,就被灌輸了大量的理論和範例。結果就是你有了一個解決方案,可是卻沒有須要使用這個解決方案的問題。安全
這篇文章將嘗試着改變這種學習流程,咱們將以一個簡單的問題做爲開始:泛型是用來作什麼的?答案是:沒有泛型,將會很難建立類型安全的集合。ide
C# 是一個類型安全的語言,類型安全容許編譯器(可信賴地)捕獲潛在的錯誤,而不是在程序運行時才發現(不可信賴地,每每發生在你將產品出售了之後!)。所以,在C#中,全部的變量都有一個定義了的類型;當你將一個對象賦值給那個變量的時候,編譯器檢查這個賦值是否正確,若是有問題,將會給出錯誤信息。函數
在 .Net 1.1 版本(2003)中,當你在使用集合時,這種類型安全就失效了。由.Net 類庫提供的全部關於集合的類全是用來存儲基類型(Object)的,而.Net中全部的一切都是由Object基類繼承下來的,所以全部類型均可以放到一個集合中。因而,至關於根本就沒有了類型檢測。post
更糟的是,每一次你從集合中取出一個Object,你都必須將它強制轉換成正確的類型,這一轉換將對性能形成影響,而且產生冗長的代碼(若是你忘了進行轉換,將會拋出異常)。更進一步地講,若是你給集合中添加一個值類型(好比,一個整型變量),這個整型變量就被隱式地裝箱了(再一次下降了性能),而當你從集合中取出它的時候,又會進行一次顯式地拆箱(又一次性能的下降和類型轉換)。性能
關於裝箱、拆箱的更多內容,請訪問 陷阱4,警戒隱式的裝箱、拆箱。學習
建立一個簡單的線性鏈表
爲了生動地感覺一下這些問題,咱們將建立一個儘量簡單的線性鏈表。對於閱讀本文的那些從未建立過線性鏈表的人。你能夠將線性鏈表想像成有一條鏈子栓在一塊兒的盒子(稱做一個結點),每一個盒子裏包含着一些數據 和 連接到這個鏈子上的下一個盒子的引用(固然,除了最後一個盒子,這個盒子對於下一個盒子的引用被設置成NULL)。測試
爲了建立咱們的簡單線性鏈表,咱們須要下面三個類:this
一、Node 類,包含數據以及下一個Node的引用。
二、LinkedList 類,包含鏈表中的第一個Node,以及關於鏈表的任何附加信息。
三、測試程序,用於測試 LinkedList 類。
爲了查看連接表如何運做,咱們添加Objects的兩種類型到鏈表中:整型 和 Employee類型。你能夠將Employee類型想象成一個包含關於公司中某一個員工全部信息的類。出於演示的目的,Employee類很是的簡單。
public class Employee{
private string name;
public Employee (string name){
this.name = name;
}
public override string ToString(){
return this.name;
}
}
這個類僅包含一個表示員工名字的字符串類型,一個設置員工名字的構造函數,一個返回Employee名字的ToString()方法。
連接表自己是由不少的Node構成,這些Note,如上面所說,必須包含數據(整型 和 Employee)和鏈表中下一個Node的引用。
public class Node{
Object data;
Node next;
public Node(Object data){
this.data = data;
this.next = null;
}
public Object Data{
get { return this.data; }
set { data = value; }
}
public Node Next{
get { return this.next; }
set { this.next = value; }
}
}
注意構造函數將私有的數據成員設置成傳遞進來的對象,而且將 next 字段設置成null。
這個類還包括一個方法,Append,這個方法接受一個Node類型的參數,咱們將把傳遞進來的Node添加到列表中的最後位置。這過程是這樣的:首先檢測當前Node的next字段,看它是否是null。若是是,那麼當前Node就是最後一個Node,咱們將當前Node的next屬性指向傳遞進來的新結點,這樣,咱們就把新Node插入到了鏈表的尾部。
若是當前Node的next字段不是null,說明當前node不是鏈表中的最後一個node。由於next字段的類型也是node,因此咱們調用next字段的Append方法(注:遞歸調用),再一次傳遞Node參數,這樣繼續下去,直到找到最後一個Node爲止。
public void Append(Node newNode){
if ( this.next == null ){
this.next = newNode;
}else{
next.Append(newNode);
}
}
Node 類中的 ToString() 方法也被覆蓋了,用於輸出 data 中的值,而且調用下一個 Node 的 ToString()方法(譯註:再一次遞歸調用)。
public override string ToString(){
string output = data.ToString();
if ( next != null ){
output += ", " + next.ToString();
}
return output;
}
這樣,當你調用第一個Node的ToString()方法時,將打印出全部鏈表上Node的值。
LinkedList 類自己只包含對一個Node的引用,這個Node稱做 HeadNode,是鏈表中的第一個Node,初始化爲null。
public class LinkedList{
Node headNode = null;
}
LinkedList 類不須要構造函數(使用編譯器建立的默認構造函數),可是咱們須要建立一個公共方法,Add(),這個方法把 data存儲到線性鏈表中。這個方法首先檢查headNode是否是null,若是是,它將使用data建立結點,並將這個結點做爲headNode,若是不是null,它將建立一個新的包含data的結點,並調用headNode的Append方法,以下面的代碼所示:
public void Add(Object data){
if ( headNode == null ){
headNode = new Node(data);
}else{
headNode.Append(new Node(data));
}
}
爲了提供一點集合的感受,咱們爲線性鏈表建立一個索引器。
public object this[ int index ]{
get{
int ctr = 0;
Node node = headNode;
while ( node != null && ctr <= index ){
if ( ctr == index ){
return node.Data;
}else{
node = node.Next;
}
ctr++;
}
return null;
}
}
最後,ToString()方法再一次被覆蓋,用以調用headNode的ToString()方法。
public override string ToString(){
if ( this.headNode != null ){
return this.headNode.ToString();
}else{
return string.Empty;
}
}
測試線性鏈表
咱們能夠添加一些整型值到鏈表中進行測試:
public void Run(){
LinkedList ll = new LinkedList();
for ( int i = 0; i < 10; i ++ ){
ll.Add(i);
}
Console.WriteLine(ll);
Console.WriteLine(" Done. Adding employees...");
}
若是你對這段代碼進行測試,它會如預計的那樣工做:
0, 1, 2, 3, 4, 5, 6, 7, 8, 9
Done. Adding employees...
然而,由於這是一個Object類型的集合,因此你一樣能夠將Employee類型添加到集合中。
ll.Add(new Employee("John"));
ll.Add(new Employee("Paul"));
ll.Add(new Employee("George"));
ll.Add(new Employee("Ringo"));
Console.WriteLine(ll);
Console.WriteLine(" Done.");
輸出的結果證明了,整型值和Employee類型都被存儲在了同一個集合中。
0, 1, 2, 3, 4, 5, 6, 7, 8, 9
Done. Adding employees...
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, John, Paul, George, Ringo
Done.
雖然看上去這樣很方便,可是負面影響是,你失去了全部類型安全的特性。由於線性鏈表須要的是一個Object類型,每個添加到集合中的整型值都被隱式裝箱了,如同 IL 代碼所示:
IL_000c: box [mscorlib]System.Int32
IL_0011: callvirt instance void ObjectLinkedList.LinkedList::Add(object)
一樣,若是上面所說,當你從你的列表中取出項目的時候,這些整型必須被顯式地拆箱(強制轉換成整型),Employee類型必須被強制轉換成 Employee類型。
Console.WriteLine("The fourth integer is " + Convert.ToInt32(ll[3]));
Employee d = (Employee) ll[11];
Console.WriteLine("The second Employee is " + d);
這些問題的解決方案是建立一個類型安全的集合。一個 Employee 線性鏈表將不能接受 Object 類型;它只接受 Employee類的實例(或者繼承自Employee類的實例)。這樣將會是類型安全的,而且再也不須要類型轉換。一個 整型的 線性鏈表,這個鏈表將再也不須要裝箱和拆箱的操做(由於它只能接受整型值)。
做爲示例,你將建立一個 EmployeeNode,該結點知道它的data的類型是Employee。
public class EmployeeNode {
Employee employeedata;
EmployeeNode employeeNext;
}
Append 方法如今接受一個 EmployeeNode 類型的參數。你一樣須要建立一個新的 EmployeeLinkedList ,這個鏈表接受一個新的 EmployeeNode:
public class EmployeeLinkedList{
EmployeeNode headNode = null;
}
EmployeeLinkedList.Add()方法再也不接受一個 Object,而是接受一個Employee:
public void Add(Employee data){
if ( headNode == null ){
headNode = new EmployeeNode(data);}
else{
headNode.Append(new EmployeeNode(data));
}
}
相似的,索引器必須被修改爲接受 EmployeeNode 類型,等等。這樣確實解決了裝箱、拆箱的問題,而且加入了類型安全的特性。你如今能夠添加Employee(但不是整型)到你新的線性鏈表中了,而且當你從中取出Employee的時候,再也不須要類型轉換了。
EmployeeLinkedList employees = new EmployeeLinkedList();
employees.Add(new Employee("Stephen King"));
employees.Add(new Employee("James Joyce"));
employees.Add(new Employee("William Faulkner"));
/* employees.Add(5); // try to add an integer - won't compile */
Console.WriteLine(employees);
Employee e = employees[1];
Console.WriteLine("The second Employee is " + e);
這樣多好啊,當有一個整型試圖隱式地轉換到Employee類型時,代碼甚至連編譯器都不能經過!
但它很差的地方是:每次你須要建立一個類型安全的列表時,你都須要作不少的複製/粘貼 。一點也不夠好,一點也沒有代碼重用。同時,若是你是這個類的做者,你甚至不能提早欲知這個連接列表所應該接受的類型是什麼,因此,你不得不將添加類型安全這一機制的工做交給類的使用者---你的用戶。
使用泛型來達到代碼重用
解決方案,如同你所猜測的那樣,就是使用泛型。經過泛型,你從新得到了連接列表的 代碼通用(對於全部類型只用實現一次),而當你初始化鏈表的時候你告訴鏈表所能接受的類型。這個實現是很是簡單的,讓咱們從新回到Node類:
public class Node{
Object data;
...
注意到 data 的類型是Object,(在EmployeeNode中,它是Employee)。咱們將把它變成一個泛型(一般,由一個大寫的T表明)。咱們一樣定義Node類,表示它能夠被泛型化,以接受一個T類型。
public class Node <T>{
T data;
...
讀做:T類型的Node。T表明了當Node被初始化時,Node所接受的類型。T能夠是Object,也多是整型或者是Employee。這個在Node被初始化的時候才能肯定。
注意:使用T做爲標識只是一種約定俗成,你可使用其餘的字母組合來代替,好比這樣:
public class Node <UnknownType>{
UnknownType data;
...
經過使用T做爲未知類型,next字段(下一個結點的引用)必須被聲明爲T類型的Node(意思是說接受一個T類型的泛型化Node)。
Node<T> next;
構造函數接受一個T類型的簡單參數:
public Node(T data)
{
this.data = data;
this.next = null;
}
Node 類的其他部分是很簡單的,全部你須要使用Object的地方,你如今都須要使用T。LinkedList 類如今接受一個 T類型的Node,而不是一個簡單的Node做爲頭結點。
public class LinkedList<T>{
Node<T> headNode = null;
再來一遍,轉換是很直白的。任何地方你須要使用Object的,如今改作T,任何須要使用Node的地方,如今改作 Node<T>。下面的代碼初始化了兩個連接表。一個是整型的。
LinkedList<int> ll = new LinkedList<int>();
另外一個是Employee類型的:
LinkedList<Employee> employees = new LinkedList<Employee>();
剩下的代碼與第一個版本沒有區別,除了沒有裝箱、拆箱,並且也不可能將錯誤的類型保存到集合中。
LinkedList<int> ll = new LinkedList<int>();
for ( int i = 0; i < 10; i ++ )
{
ll.Add(i);
}
Console.WriteLine(ll);
Console.WriteLine(" Done.");
LinkedList<Employee> employees = new LinkedList<Employee>();
employees.Add(new Employee("John"));
employees.Add(new Employee("Paul"));
employees.Add(new Employee("George"));
employees.Add(new Employee("Ringo"));
Console.WriteLine(employees);
Console.WriteLine(" Done.");
Console.WriteLine("The fourth integer is " + ll[3]);
Employee d = employees[1];
Console.WriteLine("The second Employee is " + d);
泛型容許你不用複製/粘貼冗長的代碼就實現類型安全的集合。並且,由於泛型是在運行時才被擴展成特殊類型。Just In Time編譯器能夠在不一樣的實例之間共享代碼,最後,它顯著地減小了你須要編寫的代碼。