【小松教你手遊開發】【面試必讀(程式設計基礎)】 轉載一篇優秀的c#泛型理解
http://www.tracefact.net/CSharp-Programming/Generics-In-CSharp.aspx
術語表
generics:泛型
type-safe:型別安全
collection: 集合
compiler:編譯器
run time:程式執行時
object: 物件
.NET library:.Net類庫
value type: 值型別
box: 裝箱
unbox: 拆箱
implicity: 隱式
explicity: 顯式
linked list: 線性連結串列
node: 結點
indexer: 索引器
簡介
Visual C# 2.0 的一個最受期待的(或許也是最讓人畏懼)的一個特性就是對於泛型的支援。這篇文章將告訴你泛型用來解決什麼樣的問題,以及如何使用它們來提高你的程式碼質量,還有你不必恐懼泛型的原因。
泛型是什麼?
很多人覺得泛型很難理解。我相信這是因為他們通常在瞭解泛型是用來解決什麼問題之前,就被灌輸了大量的理論和範例。結果就是你有了一個解決方案,但是卻沒有需要使用這個解決方案的問題。
這篇文章將嘗試著改變這種學習流程,我們將以一個簡單的問題作為開始:泛型是用來做什麼的?答案是:沒有泛型,將會很難建立型別安全的集合。
C# 是一個型別安全的語言,型別安全允許編譯器(可信賴地)捕獲潛在的錯誤,而不是在程式執行時才發現(不可信賴地,往往發生在你將產品出售了以後!)。因此,在C#中,所有的變數都有一個定義了的型別;當你將一個物件賦值給那個變數的時候,編譯器檢查這個賦值是否正確,如果有問題,將會給出錯誤資訊。
在 .Net 1.1 版本(2003)中,當你在使用集合時,這種型別安全就失效了。由.Net 類庫提供的所有關於集合的類全是用來儲存基型別(Object)的,而.Net中所有的一切都是由Object基類繼承下來的,因此所有型別都可以放到一個集合中。於是,相當於根本就沒有了型別檢測。
更糟的是,每一次你從集合中取出一個Object,你都必須將它強制轉換成正確的型別,這一轉換將對效能造成影響,並且產生冗長的程式碼(如果你忘了進行轉換,將會丟擲異常)。更進一步地講,如果你給集合中新增一個值型別(比如,一個整型變數),這個整型變數就被隱式地裝箱了(再一次降低了效能),而當你從集合中取出它的時候,又會進行一次顯式地拆箱(又一次效能的降低和型別轉換)。
關於裝箱、拆箱的更多內容,請訪問 陷阱4,警惕隱式的裝箱、拆箱。
建立一個簡單的線性連結串列
為了生動地感受一下這些問題,我們將建立一個儘可能簡單的線性連結串列。對於閱讀本文的那些從未建立過線性連結串列的人。你可以將線性連結串列想像成有一條鏈子栓在一起的盒子(稱作一個結點),每個盒子裡包含著一些資料 和 連結到這個鏈子上的下一個盒子的引用(當然,除了最後一個盒子,這個盒子對於下一個盒子的引用被設定成NULL)。
為了建立我們的簡單線性連結串列,我們需要下面三個類:
1、Node 類,包含資料以及下一個Node的引用。
2、LinkedList 類,包含連結串列中的第一個Node,以及關於連結串列的任何附加資訊。
3、測試程式,用於測試 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編譯器可以在不同的例項之間共享程式碼,最後,它顯著地減少了你需要編寫的程式碼。
相關文章
- C#基礎:泛型的理解和使用C#泛型
- 【小松教你手遊開發】【unity實用技能】重置scrollpanelUnity
- C#基礎:泛型委託C#泛型
- C++ 泛型程式設計基礎:模板通識C++泛型程式設計
- [.net 物件導向程式設計基礎] (18) 泛型物件程式設計泛型
- Java基礎——深入理解泛型Java泛型
- 泛型最佳實踐:Go泛型設計者教你如何用泛型泛型Go
- 【小松教你手遊開發】【unity實用技能】c++ 交叉引用解決方法UnityC++
- 【小松教你手遊開發】【unity實用技能】unity 記憶體除錯方法Unity記憶體除錯
- 優秀程式設計師必備的10個技能程式設計師
- 優秀程式設計師必備的15大技能程式設計師
- .NET泛型程式設計簡介 (轉)泛型程式設計
- 泛型程式設計泛型程式設計
- 畫江湖之面試篇 [第一篇] tip:基礎是每個程式設計師必須的面試程式設計師
- 優秀程式設計師的優秀歷程程式設計師
- 優秀程式設計師必須知道的32個演算法,提高你的開發效率程式設計師演算法
- 優秀程式設計師之道:深入理解你的程式碼程式設計師
- Java程式設計師轉Android開發必讀經驗分享Java程式設計師Android
- 【小松教你手遊開發】【unity實用技能】NGUI Scrollview的Reposition的幾個總結UnityNGUIView
- c# windows程式設計基礎C#Windows程式設計
- java 泛型程式設計Java泛型程式設計
- 優秀程式設計師必備的23條好習慣程式設計師
- java 基礎 泛型Java泛型
- Java基礎-泛型Java泛型
- JavaSE基礎:泛型Java泛型
- TypeScript基礎--泛型TypeScript泛型
- 【Java基礎】泛型Java泛型
- 理解C#泛型運作原理C#泛型
- Java程式設計師必讀:最新流行的Java開發程式設計技術Java程式設計師
- 提給程式設計師的10道Java泛型面試題程式設計師Java泛型面試題
- C++ STL與泛型程式設計-第一篇 (Boolan)C++泛型程式設計
- 做個優秀的市場程式設計師 (轉)程式設計師
- 優秀程式設計師因何而優秀?程式設計師
- 女程式設計師必須證明自己是真正的優秀程式設計師
- ffmpeg基礎庫程式設計開發電子書pdf下載程式設計
- 優秀程式設計的“藝術”程式設計
- [CUJ]泛型程式設計--轉移建構函式 (轉)泛型程式設計函式
- 十、GO程式設計模式 : 泛型程式設計Go程式設計設計模式泛型