出處:http://www.ondotnet.com/pub/a/dotnet/2004/05/17/liberty.html
generics:泛型
type-safe:類型安全
collection: 集合
compiler:編譯器
run time:程序運(yùn)行時(shí)
object: 對(duì)象
.NET library:.Net類庫(kù)
value type: 值類型
box: 裝箱
unbox: 拆箱
implicity: 隱式
explicity: 顯式
linked list: 線性鏈表
node: 結(jié)點(diǎn)
indexer: 索引器
Visual C# 2.0 的一個(gè)最受期待的(或許也是最讓人畏懼)的一個(gè)特性就是對(duì)于泛型的支持。這篇文章將告訴你泛型用來(lái)解決什么樣的問(wèn)題,以及如何使用它們來(lái)提高你的代碼質(zhì)量,還有你不必恐懼泛型的原因。
很多人覺(jué)得泛型很難理解。我相信這是因?yàn)樗麄兺ǔT诹私夥盒褪怯脕?lái)解決什么問(wèn)題之前,就被灌輸了大量的理論和范例。結(jié)果就是你有了一個(gè)解決方案,但是卻沒(méi)有需要使用這個(gè)解決方案的問(wèn)題。
這篇文章將嘗試著改變這種學(xué)習(xí)流程,我們將以一個(gè)簡(jiǎn)單的問(wèn)題作為開(kāi)始:泛型是用來(lái)做什么的?答案是:沒(méi)有泛型,將會(huì)很難創(chuàng)建類型安全的集合。
C# 是一個(gè)類型安全的語(yǔ)言,類型安全允許編譯器(可信賴地)捕獲潛在的錯(cuò)誤,而不是在程序運(yùn)行時(shí)才發(fā)現(xiàn)(不可信賴地,往往發(fā)生在你將產(chǎn)品出售了以后!)。因此,在C#中,所有的變量都有一個(gè)定義了的類型;當(dāng)你將一個(gè)對(duì)象賦值給那個(gè)變量的時(shí)候,編譯器檢查這個(gè)賦值是否正確,如果有問(wèn)題,將會(huì)給出錯(cuò)誤信息。
在 .Net 1.1 版本(2003)中,當(dāng)你在使用集合時(shí),這種類型安全就失效了。由.Net 類庫(kù)提供的所有關(guān)于集合的類全是用來(lái)存儲(chǔ)基類型(Object)的,而.Net中所有的一切都是由Object基類繼承下來(lái)的,因此所有類型都可以放到一個(gè)集合中。于是,相當(dāng)于根本就沒(méi)有了類型檢測(cè)。
更糟的是,每一次你從集合中取出一個(gè)Object,你都必須將它強(qiáng)制轉(zhuǎn)換成正確的類型,這一轉(zhuǎn)換將對(duì)性能造成影響,并且產(chǎn)生冗長(zhǎng)的代碼(如果你忘了進(jìn)行轉(zhuǎn)換,將會(huì)拋出異常)。更進(jìn)一步地講,如果你給集合中添加一個(gè)值類型(比如,一個(gè)整型變量),這個(gè)整型變量就被隱式地裝箱了(再一次降低了性能),而當(dāng)你從集合中取出它的時(shí)候,又會(huì)進(jìn)行一次顯式地拆箱(又一次性能的降低和類型轉(zhuǎn)換)。
關(guān)于裝箱、拆箱的更多內(nèi)容,請(qǐng)?jiān)L問(wèn) 陷阱4,警惕隱式的裝箱、拆箱。
為了生動(dòng)地感受一下這些問(wèn)題,我們將創(chuàng)建一個(gè)盡可能簡(jiǎn)單的線性鏈表。對(duì)于閱讀本文的那些從未創(chuàng)建過(guò)線性鏈表的人。你可以將線性鏈表想像成有一條鏈子栓在一起的盒子(稱作一個(gè)結(jié)點(diǎn)),每個(gè)盒子里包含著一些數(shù)據(jù) 和 鏈接到這個(gè)鏈子上的下一個(gè)盒子的引用(當(dāng)然,除了最后一個(gè)盒子,這個(gè)盒子對(duì)于下一個(gè)盒子的引用被設(shè)置成NULL)。
為了創(chuàng)建我們的簡(jiǎn)單線性鏈表,我們需要下面三個(gè)類:
1、Node 類,包含數(shù)據(jù)以及下一個(gè)Node的引用。
2、LinkedList 類,包含鏈表中的第一個(gè)Node,以及關(guān)于鏈表的任何附加信息。
3、測(cè)試程序,用于測(cè)試 LinkedList 類。
為了查看鏈接表如何運(yùn)作,我們添加Objects的兩種類型到鏈表中:整型 和 Employee類型。你可以將Employee類型想象成一個(gè)包含關(guān)于公司中某一個(gè)員工所有信息的類。出于演示的目的,Employee類非常的簡(jiǎn)單。
public class Employee{
private string name;
public Employee (string name){
this.name = name;
}
public override string ToString(){
return this.name;
}
}
這個(gè)類僅包含一個(gè)表示員工名字的字符串類型,一個(gè)設(shè)置員工名字的構(gòu)造函數(shù),一個(gè)返回Employee名字的ToString()方法。
鏈接表本身是由很多的Node構(gòu)成,這些Note,如上面所說(shuō),必須包含數(shù)據(jù)(整型 和 Employee)和鏈表中下一個(gè)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; }
}
}
注意構(gòu)造函數(shù)將私有的數(shù)據(jù)成員設(shè)置成傳遞進(jìn)來(lái)的對(duì)象,并且將 next 字段設(shè)置成null。
這個(gè)類還包括一個(gè)方法,Append,這個(gè)方法接受一個(gè)Node類型的參數(shù),我們將把傳遞進(jìn)來(lái)的Node添加到列表中的最后位置。這過(guò)程是這樣的:首先檢測(cè)當(dāng)前Node的next字段,看它是不是null。如果是,那么當(dāng)前Node就是最后一個(gè)Node,我們將當(dāng)前Node的next屬性指向傳遞進(jìn)來(lái)的新結(jié)點(diǎn),這樣,我們就把新Node插入到了鏈表的尾部。
如果當(dāng)前Node的next字段不是null,說(shuō)明當(dāng)前node不是鏈表中的最后一個(gè)node。因?yàn)閚ext字段的類型也是node,所以我們調(diào)用next字段的Append方法(注:遞歸調(diào)用),再一次傳遞Node參數(shù),這樣繼續(xù)下去,直到找到最后一個(gè)Node為止。
public void Append(Node newNode){
if ( this.next == null ){
this.next = newNode;
}else{
next.Append(newNode);
}
}
Node 類中的 ToString() 方法也被覆蓋了,用于輸出 data 中的值,并且調(diào)用下一個(gè) Node 的 ToString()方法(譯注:再一次遞歸調(diào)用)。
public override string ToString(){
string output = data.ToString();
if ( next != null ){
output += ", " + next.ToString();
}
return output;
}
這樣,當(dāng)你調(diào)用第一個(gè)Node的ToString()方法時(shí),將打印出所有鏈表上Node的值。
LinkedList 類本身只包含對(duì)一個(gè)Node的引用,這個(gè)Node稱作 HeadNode,是鏈表中的第一個(gè)Node,初始化為null。
public class LinkedList{
Node headNode = null;
}
LinkedList 類不需要構(gòu)造函數(shù)(使用編譯器創(chuàng)建的默認(rèn)構(gòu)造函數(shù)),但是我們需要?jiǎng)?chuàng)建一個(gè)公共方法,Add(),這個(gè)方法把 data存儲(chǔ)到線性鏈表中。這個(gè)方法首先檢查headNode是不是null,如果是,它將使用data創(chuàng)建結(jié)點(diǎn),并將這個(gè)結(jié)點(diǎn)作為headNode,如果不是null,它將創(chuàng)建一個(gè)新的包含data的結(jié)點(diǎn),并調(diào)用headNode的Append方法,如下面的代碼所示:
public void Add(Object data){
if ( headNode == null ){
headNode = new Node(data);
}else{
headNode.Append(new Node(data));
}
}
為了提供一點(diǎn)集合的感覺(jué),我們?yōu)榫€性鏈表創(chuàng)建一個(gè)索引器。
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()方法再一次被覆蓋,用以調(diào)用headNode的ToString()方法。
public override string ToString(){
if ( this.headNode != null ){
return this.headNode.ToString();
}else{
return string.Empty;
}
}
我們可以添加一些整型值到鏈表中進(jìn)行測(cè)試:
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...");
}
如果你對(duì)這段代碼進(jìn)行測(cè)試,它會(huì)如預(yù)計(jì)的那樣工作:
0, 1, 2, 3, 4, 5, 6, 7, 8, 9
Done. Adding employees...
然而,因?yàn)檫@是一個(gè)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.");
輸出的結(jié)果證實(shí)了,整型值和Employee類型都被存儲(chǔ)在了同一個(gè)集合中。
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.
雖然看上去這樣很方便,但是負(fù)面影響是,你失去了所有類型安全的特性。因?yàn)榫€性鏈表需要的是一個(gè)Object類型,每一個(gè)添加到集合中的整型值都被隱式裝箱了,如同 IL 代碼所示:
IL_
IL_0011: callvirt instance void ObjectLinkedList.LinkedList::Add(object)
同樣,如果上面所說(shuō),當(dāng)你從你的列表中取出項(xiàng)目的時(shí)候,這些整型必須被顯式地拆箱(強(qiáng)制轉(zhuǎn)換成整型),Employee類型必須被強(qiáng)制轉(zhuǎn)換成 Employee類型。
Console.WriteLine("The fourth integer is " + Convert.ToInt32(ll[3]));
Employee d = (Employee) ll[11];
Console.WriteLine("The second Employee is " + d);
這些問(wèn)題的解決方案是創(chuàng)建一個(gè)類型安全的集合。一個(gè) Employee 線性鏈表將不能接受 Object 類型;它只接受 Employee類的實(shí)例(或者繼承自Employee類的實(shí)例)。這樣將會(huì)是類型安全的,并且不再需要類型轉(zhuǎn)換。一個(gè) 整型的 線性鏈表,這個(gè)鏈表將不再需要裝箱和拆箱的操作(因?yàn)樗荒芙邮苷椭?。
作為示例,你將創(chuàng)建一個(gè) EmployeeNode,該結(jié)點(diǎn)知道它的data的類型是Employee。
public class EmployeeNode {
Employee employeedata;
EmployeeNode employeeNext;
}
Append 方法現(xiàn)在接受一個(gè) EmployeeNode 類型的參數(shù)。你同樣需要?jiǎng)?chuàng)建一個(gè)新的 EmployeeLinkedList ,這個(gè)鏈表接受一個(gè)新的 EmployeeNode:
public class EmployeeLinkedList{
EmployeeNode headNode = null;
}
EmployeeLinkedList.Add()方法不再接受一個(gè) Object,而是接受一個(gè)Employee:
public void Add(Employee data){
if ( headNode == null ){
headNode = new EmployeeNode(data);}
else{
headNode.Append(new EmployeeNode(data));
}
}
類似的,索引器必須被修改成接受 EmployeeNode 類型,等等。這樣確實(shí)解決了裝箱、拆箱的問(wèn)題,并且加入了類型安全的特性。你現(xiàn)在可以添加Employee(但不是整型)到你新的線性鏈表中了,并且當(dāng)你從中取出Employee的時(shí)候,不再需要類型轉(zhuǎn)換了。
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);
這樣多好啊,當(dāng)有一個(gè)整型試圖隱式地轉(zhuǎn)換到Employee類型時(shí),代碼甚至連編譯器都不能通過(guò)!
但它不好的地方是:每次你需要?jiǎng)?chuàng)建一個(gè)類型安全的列表時(shí),你都需要做很多的復(fù)制/粘貼 。一點(diǎn)也不夠好,一點(diǎn)也沒(méi)有代碼重用。同時(shí),如果你是這個(gè)類的作者,你甚至不能提前欲知這個(gè)鏈接列表所應(yīng)該接受的類型是什么,所以,你不得不將添加類型安全這一機(jī)制的工作交給類的使用者---你的用戶。
解決方案,如同你所猜想的那樣,就是使用泛型。通過(guò)泛型,你重新獲得了鏈接列表的 代碼通用(對(duì)于所有類型只用實(shí)現(xiàn)一次),而當(dāng)你初始化鏈表的時(shí)候你告訴鏈表所能接受的類型。這個(gè)實(shí)現(xiàn)是非常簡(jiǎn)單的,讓我們重新回到Node類:
public class Node{
Object data;
...
注意到 data 的類型是Object,(在EmployeeNode中,它是Employee)。我們將把它變成一個(gè)泛型(通常,由一個(gè)大寫(xiě)的T代表)。我們同樣定義Node類,表示它可以被泛型化,以接受一個(gè)T類型。
public class Node <T>{
T data;
...
讀作:T類型的Node。T代表了當(dāng)Node被初始化時(shí),Node所接受的類型。T可以是Object,也可能是整型或者是Employee。這個(gè)在Node被初始化的時(shí)候才能確定。
注意:使用T作為標(biāo)識(shí)只是一種約定俗成,你可以使用其他的字母組合來(lái)代替,比如這樣:
public class Node <UnknownType>{
UnknownType data;
...
通過(guò)使用T作為未知類型,next字段(下一個(gè)結(jié)點(diǎn)的引用)必須被聲明為T(mén)類型的Node(意思是說(shuō)接受一個(gè)T類型的泛型化Node)。
Node<T> next;
構(gòu)造函數(shù)接受一個(gè)T類型的簡(jiǎn)單參數(shù):
public Node(T data)
{
this.data = data;
this.next = null;
}
Node 類的其余部分是很簡(jiǎn)單的,所有你需要使用Object的地方,你現(xiàn)在都需要使用T。LinkedList 類現(xiàn)在接受一個(gè) T類型的Node,而不是一個(gè)簡(jiǎn)單的Node作為頭結(jié)點(diǎn)。
public class LinkedList<T>{
Node<T> headNode = null;
再來(lái)一遍,轉(zhuǎn)換是很直白的。任何地方你需要使用Object的,現(xiàn)在改做T,任何需要使用Node的地方,現(xiàn)在改做 Node<T>。下面的代碼初始化了兩個(gè)鏈接表。一個(gè)是整型的。
LinkedList<int> ll = new LinkedList<int>();
另一個(gè)是Employee類型的:
LinkedList<Employee> employees = new LinkedList<Employee>();
剩下的代碼與第一個(gè)版本沒(méi)有區(qū)別,除了沒(méi)有裝箱、拆箱,而且也不可能將錯(cuò)誤的類型保存到集合中。
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);
泛型允許你不用復(fù)制/粘貼冗長(zhǎng)的代碼就實(shí)現(xiàn)類型安全的集合。而且,因?yàn)榉盒褪窃谶\(yùn)行時(shí)才被擴(kuò)展成特殊類型。Just In Time編譯器可以在不同的實(shí)例之間共享代碼,最后,它顯著地減少了你需要編寫(xiě)的代碼。
聯(lián)系客服