C#泛型的學習體會
1.簡介
所謂泛型,即通過引數化型別來實現在同一份程式碼上操作多種資料型別。
泛型程式設計是一種程式設計正規化,它利用“引數化型別”將型別抽象化,從而實現更為靈活的複用。
C#泛型賦予了程式碼更強的型別安全,更好的複用,更高的效率,更清晰的約束。 這麼多好處,應該能吸引你看下去了吧?
2.泛型問題陳述
考慮一種普通的、提供傳統 Push() 和 Pop() 方法的資料結構(例如,堆疊)。在開發通用堆疊時,您可能願意使用它來儲存各種型別的例項。
public class Stack
{
object[] m_Items;
public void Push(object item)
{...}
public object Pop()
{...}
}
程式碼塊 1 顯示基於 Object 的堆疊的完整實現。因為 Object 是規範的 .NET 基型別,所以您可以使用基於 Object 的堆疊來保持任何型別的項(例如,整數):
Stack stack = new Stack(); stack.Push(1); stack.Push(2); int number = (int)stack.Pop();
程式碼塊 1. 基於 Object 的堆疊
public class Stack { readonly int m_Size; int m_StackPointer = 0; object[] m_Items; public Stack():this(100) {} public Stack(int size) { m_Size = size; m_Items = new object[m_Size]; } public void Push(object item) { if(m_StackPointer >= m_Size) throw new StackOverflowException(); m_Items[m_StackPointer] = item; m_StackPointer++; } public object Pop() { m_StackPointer--; if(m_StackPointer >= 0) { return m_Items[m_StackPointer]; } else { m_StackPointer = 0; throw new InvalidOperationException("Cannot pop an empty stack"); } } }
基於Object 的解決方案存在兩個問題。
第一個問題是效能。
在使用值型別時,必須將它們裝箱以便推送和儲存它們,並且在將值型別彈出堆疊時將其取消裝箱。裝箱和取消裝箱都會根據它們自己的許可權造成重大的效能損失,但是它還會增加託管堆上的壓力,導致更多的垃圾收集工作,而這對於效能而言也不太好。即使是在使用引用型別而不是值型別時,仍然存在效能損失,這是因為必須從 Object 向您要與之互動的實際型別進行強制型別轉換,從而造成強制型別轉換開銷:
Stack stack = new Stack(); stack.Push("1"); string number = (string)stack.Pop();
第二個問題(通常更為嚴重)是型別安全。
因為編譯器允許在任何型別和 Object 之間進行強制型別轉換,所以您將丟失編譯時型別安全。例如,以下程式碼可以正確編譯,但是在執行時將引發無效強制型別轉換異常:
Stack stack = new Stack(); stack.Push(1); //This compiles, but is not type safe, and will throw an exception: string number = (string)stack.Pop();
您可以通過提供型別特定的(因而是型別安全的)高效能堆疊來克服上述兩個問題。對於整型,可以實現並使用 IntStack:
public class IntStack { int[] m_Items; public void Push(int item){...} public int Pop(){...} } IntStack stack = new IntStack(); stack.Push(1); int number = stack.Pop();
對於字串,可以實現 StringStack:
public class StringStack { string[] m_Items; public void Push(string item){...} public string Pop(){...} } StringStack stack = new StringStack(); stack.Push("1"); string number = stack.Pop();
等等。遺憾的是,以這種方式解決效能和型別安全問題,會引起第三個同樣嚴重的問題 — 影響工作效率。編寫型別特定的資料結構是一項乏味的、重複性的且易於出錯的任務。在修復該資料結構中的缺陷時,您不能只在一個位置修復該缺陷,而必須在實質上是同一資料結構的型別特定的副本所出現的每個位置進行修復。此外,沒有辦法預知未知的或尚未定義的將來型別的使用情況,因此還必須保持基於Object 的資料結構。
3.什麼是泛型
通過泛型可以定義型別安全類,而不會損害型別安全、效能或工作效率。您只須一次性地將伺服器實現為一般伺服器,同時可以用任何型別來宣告和使用它。為此,需要使用 < 和 > 括號,以便將一般型別引數括起來。例如,可以按如下方式定義和使用一般堆疊:
public class Stack { T[] m_Items; public void Push(T item) {...} public T Pop() {...} } Stack stack = new Stack(); stack.Push(1); stack.Push(2); int number = stack.Pop();
程式碼塊 2 顯示一般堆疊的完整實現。將程式碼塊 1 與程式碼塊 2 進行比較,您會看到,好像 程式碼塊 1 中每個使用 Object 的地方在程式碼塊 2 中都被替換成了 T,除了使用一般型別引數 T 定義 Stack 以外:
public class Stack {...}
在使用一般堆疊時,必須通知編譯器使用哪個型別來代替一般型別引數 T(無論是在宣告變數時,還是在例項化變數時):
Stack stack = new Stack();
編譯器和執行庫負責完成其餘工作。所有接受或返回 T 的方法(或屬性)都將改為使用指定的型別(在上述示例中為整型)。
程式碼塊 2. 一般堆疊
public class Stack { readonly int m_Size; int m_StackPointer = 0; T[] m_Items; public Stack():this(100) {} public Stack(int size) { m_Size = size; m_Items = new T[m_Size]; } public void Push(T item) { if(m_StackPointer >= m_Size) throw new StackOverflowException(); m_Items[m_StackPointer] = item; m_StackPointer++; } public T Pop() { m_StackPointer--; if(m_StackPointer >= 0) { return m_Items[m_StackPointer]; } else { m_StackPointer = 0; throw new InvalidOperationException("Cannot pop an empty stack"); } } }
注 T 是一般型別引數(或型別引數),而一般型別為 Stack。Stack 中的 int 為型別實參。
該程式設計模型的優點在於,內部演算法和資料操作保持不變,而實際資料型別可以基於客戶端使用伺服器程式碼的方式進行更改。
4.泛型實現
表面上,C# 泛型的語法看起來與 C++ 模板類似,但是編譯器實現和支援它們的方式存在重要差異。正如您將在後文中看到的那樣,這對於泛型的使用方式具有重大意義。
與 C++ 模板相比,C# 泛型可以提供增強的安全性,但是在功能方面也受到某種程度的限制。
在一些 C++ 編譯器中,在您通過特定型別使用模板類之前,編譯器甚至不會編譯模板程式碼。當您確實指定了型別時,編譯器會以內聯方式插入程式碼,並且將每個出現一般型別引數的地方替換為指定的型別。此外,每當您使用特定型別時,編譯器都會插入特定於該型別的程式碼,而不管您是否已經在應用程式中的其他某個位置為模板類指定了該型別。C++ 連結器負責解決該問題,並且並不總是有效。這可能會導致程式碼膨脹,從而增加載入時間和記憶體足跡。
在 .NET 2.0 中,泛型在 IL(中間語言)和 CLR 本身中具有本機支援。在編譯一般 C# 伺服器端程式碼時,編譯器會將其編譯為 IL,就像其他任何型別一樣。但是,IL 只包含實際特定型別的引數或佔位符。此外,一般伺服器的元資料包含一般資訊。
客戶端編譯器使用該一般元資料來支援型別安全。當客戶端提供特定型別而不是一般型別引數時,客戶端的編譯器將用指定的型別實參來替換伺服器元資料中的一般型別引數。這會向客戶端的編譯器提供型別特定的伺服器定義,就好像從未涉及到泛型一樣。這樣,客戶端編譯器就可以確保方法引數的正確性,實施型別安全檢查,甚至執行型別特定的 IntelliSense。
有趣的問題是,.NET 如何將伺服器的一般 IL 編譯為機器碼。原來,所產生的實際機器碼取決於指定的型別是值型別還是引用型別。如果客戶端指定值型別,則 JIT 編譯器將 IL 中的一般型別引數替換為特定的值型別,並且將其編譯為本機程式碼。但是,JIT 編譯器會跟蹤它已經生成的型別特定的伺服器程式碼。如果請求 JIT 編譯器用它已經編譯為機器碼的值型別編譯一般伺服器,則它只是返回對該伺服器程式碼的引用。因為 JIT 編譯器在以後的所有場合中都將使用相同的值型別特定的伺服器程式碼,所以不存在程式碼膨脹問題。
如果客戶端指定引用型別,則 JIT 編譯器將伺服器 IL 中的一般引數替換為 Object,並將其編譯為本機程式碼。在以後的任何針對引用型別而不是一般型別引數的請求中,都將使用該程式碼。請注意,採用這種方式,JIT 編譯器只會重新使用實際程式碼。例項仍然按照它們離開託管堆的大小分配空間,並且沒有強制型別轉換。
泛型的好處
.NET 中的泛型使您可以重用程式碼以及在實現它時付出的努力。型別和內部資料可以在不導致程式碼膨脹的情況下更改,而不管您使用的是值型別還是引用型別。您可以一次性地開發、測試和部署程式碼,通過任何型別(包括將來的型別)來重用它,並且全部具有編譯器支援和型別安全。因為一般程式碼不會強行對值型別進行裝箱和取消裝箱,或者對引用型別進行向下強制型別轉換,所以效能得到顯著提高。對於值型別,效能通常會提高 200%;對於引用型別,在訪問該型別時,可以預期效能最多提高 100%(當然,整個應用程式的效能可能會提高,也可能不會提高)。本文隨附的原始碼包含一個微型基準應用程式,它在緊密迴圈中執行堆疊。該應用程式使您可以在基於 Object 的堆疊和一般堆疊上試驗值型別和引用型別,以及更改迴圈迭代的次數以檢視泛型對效能產生的影響。
應用泛型
因為 IL 和 CLR 為泛型提供本機支援,所以大多數符合 CLR 的語言都可以利用一般型別。例如,下面這段 Visual Basic .NET 程式碼使用程式碼塊 2 的一般堆疊:
Dim stack As Stack(Of Integer) stack = new Stack(Of Integer) stack.Push(3) Dim number As Integer number = stack.Pop()
您可以在類和結構中使用泛型。以下是一個有用的一般點結構:
public struct Point { public T X; public T Y; }
可以使用該一般點來表示整數座標,例如:
Point point; point.X = 1; point.Y = 2;
或者,可以使用它來表示要求浮點精度的圖表座標:
Point point; point.X = 1.2; point.Y = 3.4;
除了到目前為止介紹的基本泛型語法以外,C# 2.0 還具有一些泛型特定的語法。例如,請考慮程式碼塊 2 的 Pop() 方法。假設您不希望在堆疊為空時引發異常,而是希望返回堆疊中儲存的型別的預設值。如果您使用基於 Object 的堆疊,則可以簡單地返回 null,但是您還可以通過值型別來使用一般堆疊。為了解決該問題,您可以使用 default() 運算子,它返回型別的預設值。
下面說明如何在 Pop() 方法的實現中使用預設值:
public T Pop() { m_StackPointer--; if(m_StackPointer >= 0) { return m_Items[m_StackPointer]; } else { m_StackPointer = 0; return default(T); } }
引用型別的預設值為 null,而值型別(例如,整型、列舉和結構)的預設值為全零(用零填充相應的結構)。因此,如果堆疊是用字串構建的,則 Pop() 方法在堆疊為空時返回 null;如果堆疊是用整數構建的,則 Pop() 方法在堆疊為空時返回零。
多個一般型別
單個型別可以定義多個一般型別引數。例如,請考慮程式碼塊 3 中顯示的一般連結串列。
程式碼塊 3. 一般連結串列
class Node { public K Key; public T Item; public Node NextNode; public Node() { Key = default(K); Item = defualt(T); NextNode = null; } public Node(K key,T item,Node nextNode) { Key = key; Item = item; NextNode = nextNode; } } public class LinkedList { Node m_Head; public LinkedList() { m_Head = new Node(); } public void AddHead(K key,T item) { Node newNode = new Node(key,item,m_Head.NextNode); m_Head.NextNode = newNode; } }
該連結串列儲存節點:
class Node {...}
每個節點都包含一個鍵(屬於一般型別引數 K)和一個值(屬於一般型別引數 T)。每個節點還具有對該列表中下一個節點的引用。連結串列本身根據一般型別引數 K 和 T 進行定義:
public class LinkedList {...}
這使該列表可以公開像 AddHead() 一樣的一般方法:
public void AddHead(K key,T item);
每當您宣告使用泛型的型別的變數時,都必須指定要使用的型別。但是,指定的型別實參本身可以為一般型別引數。例如,該連結串列具有一個名為 m_Head 的 Node 型別的成員變數,用於引用該列表中的第一個項。m_Head 是使用該列表自己的一般型別引數 K 和 T 宣告的。
Node m_Head;
您需要在例項化節點時提供型別實參;同樣,您可以使用該連結串列自己的一般型別引數:
public void AddHead(K key,T item) { Node newNode = new Node<K,T>(key,item,m_Head.NextNode); m_Head.NextNode = newNode; }
請注意,該列表使用與節點相同的名稱來表示一般型別引數完全是為了提高可讀性;它也可以使用其他名稱,例如:
public class LinkedList {...}
或:
public class LinkedList {...}
在這種情況下,將 m_Head 宣告為:
Node m_Head;
當客戶端使用該連結串列時,該客戶端必須提供型別實參。該客戶端可以選擇整數作為鍵,並且選擇字串作為資料項:
LinkedList list = new LinkedList(); list.AddHead(123,"AAA");
但是,該客戶端可以選擇其他任何組合(例如,時間戳)來表示鍵:
LinkedList list = new LinkedList(); list.AddHead(DateTime.Now,"AAA");
有時,為特定型別的特殊組合起別名是有用的。可以通過 using 語句完成該操作,如程式碼塊 4 中所示。請注意,別名的作用範圍是檔案的作用範圍,因此您必須按照與使用 using 名稱空間相同的方式,在專案檔案中反覆起別名。
程式碼塊 4. 一般類型別名
using List = LinkedList; class ListClient { static void Main(string[] args) { List list = new List(); list.AddHead(123,"AAA"); } }返回頁首
一般約束
使用 C# 泛型,編譯器會將一般程式碼編譯為 IL,而不管客戶端將使用什麼樣的型別實參。因此,一般程式碼可以嘗試使用與客戶端使用的特定型別實參不相容的一般型別引數的方法、屬性或成員。這是不可接受的,因為它相當於缺少型別安全。在 C# 中,您需要通知編譯器客戶端指定的型別必須遵守哪些約束,以便使它們能夠取代一般型別引數而得到使用。存在三個型別的約束。派生約束指示編譯器一般型別引數派生自諸如介面或特定基類之類的基型別。預設建構函式約束指示編譯器一般型別引數公開了預設的公共建構函式(不帶任何引數的公共建構函式)。引用/值型別約束將一般型別引數約束為引用型別或值型別。一般型別可以利用多個約束,您甚至可以在使用一般型別引數時使 IntelliSense 反射這些約束,例如,建議基型別中的方法或成員。
需要注意的是,儘管約束是可選的,但它們在開發一般型別時通常是必不可少的。沒有它們,編譯器將採取更為保守的型別安全方法,並且只允許在一般型別引數中訪問 Object 級別功能。約束是一般型別元資料的一部分,以便客戶端編譯器也可以利用它們。客戶端編譯器只允許客戶端開發人員使用遵守這些約束的型別,從而實施型別安全。
以下示例將詳細說明約束的需要和用法。假設您要向程式碼塊 3 的連結串列中新增索引功能或按鍵搜尋功能:
public class LinkedList { T Find(K key) {...} public T this[K key] { get{return Find(key);} } }
這使客戶端可以編寫以下程式碼:
LinkedList list = new LinkedList(); list.AddHead(123,"AAA"); list.AddHead(456,"BBB"); string item = list[456]; Debug.Assert(item == "BBB");
要實現搜尋,您需要掃描列表,將每個節點的鍵與您要查詢的鍵進行比較,並且返回鍵匹配的節點的項。問題在於,Find() 的以下實現無法編譯:
T Find(K key) { Node current = m_Head; while(current.NextNode != null) { if(current.Key == key) //Will not compile break; else current = current.NextNode; } return current.Item; }
原因在於,編譯器將拒絕編譯以下行:
if(current.Key == key)
上述行將無法編譯,因為編譯器不知道 K(或客戶端提供的實際型別)是否支援 == 運算子。例如,預設情況下,結構不提供這樣的實現。您可以嘗試通過使用 IComparable 介面來克服 == 運算子侷限性:
public interface IComparable { int CompareTo(object obj); }
如果您與之進行比較的物件等於實現該介面的物件,則 CompareTo() 返回 0;因此,Find() 方法可以按如下方式使用它:
if(current.Key.CompareTo(key) == 0)
遺憾的是,這也無法編譯,因為編譯器無法知道 K(或客戶端提供的實際型別)是否派生自 IComparable。
您可以顯式強制轉換到 IComparable,以強迫編譯器編譯比較行,除非這樣做需要犧牲型別安全:
if(((IComparable)(current.Key)).CompareTo(key) == 0)
如果客戶端使用的型別不是派生自 IComparable,則會導致執行時異常。此外,當所使用的鍵型別是值型別而非鍵型別引數時,您可以對該鍵執行裝箱,而這可能具有一些效能方面的影響。
派生約束
在 C# 2.0 中,可以使用 where 保留關鍵字來定義約束。在一般型別引數中使用 where 關鍵字,後面跟一個派生冒號,以指示編譯器該一般型別引數實現了特定介面。例如,以下為實現 LinkedList 的 Find() 方法所必需的派生約束:
public class LinkedList where K : IComparable { T Find(K key) { Node current = m_Head; while(current.NextNode != null) { if(current.Key.CompareTo(key) == 0) break; else current = current.NextNode; } return current.Item; } //Rest of the implementation }
您還將在您約束的介面的方法上獲得 IntelliSense 支援。
當客戶端宣告一個 LinkedList 型別的變數,以便為列表的鍵提供型別實參時,客戶端編譯器將堅持要求鍵型別派生自 IComparable,否則,將拒絕生成客戶端程式碼。
請注意,即使該約束允許您使用 IComparable,它也不會在所使用的鍵是值型別(例如,整型)時,消除裝箱所帶來的效能損失。為了克服該問題,System.Collections.Generic 名稱空間定義了一般介面 IComparable:
public interface IComparable { int CompareTo(T other); bool Equals(T other); }
您可以約束鍵型別引數以支援 IComparable,並且使用鍵的型別作為型別引數;這樣,您不僅獲得了型別安全,而且消除了在值型別用作鍵時的裝箱操作:
public class LinkedList where K : IComparable {...}
實際上,所有支援 .NET 1.1 中的 IComparable 的型別都支援 .NET 2.0 中的 IComparable。這使得可以使用常見型別(例如,int、string、GUID、DateTime 等等)的鍵。
在 C# 2.0 中,所有約束都必須出現在一般類的實際派生列表之後。例如,如果 LinkedList 派生自 IEnumerable 介面(以獲得迭代器支援),則需要將 where 關鍵字放在緊跟它後面的位置:
public class LinkedList : IEnumerable where K : IComparable {...}
通常,只須在需要的級別定義約束。在連結串列示例中,在節點級別定義 IComparable 派生約束是沒有意義的,因為節點本身不會比較鍵。如果您這樣做,則您還必須將該約束放在 LinkedList 級別,即使該列表不比較鍵。這是因為該列表包含一個節點作為成員變數,從而導致編譯器堅持要求:在列表級別定義的鍵型別必須遵守該節點在一般鍵型別上放置的約束。
換句話說,如果您按如下方式定義該節點:
class Node where K : IComparable {...}
則您必須在列表級別重複該約束,即使您不提供 Find() 方法或其他任何與此有關的方法:
public class LinkedList where KeyType : IComparable { Node<KeyType,DataType> m_Head; }
您可以在同一個一般型別引數上約束多個介面(彼此用逗號分隔)。例如:
public class LinkedList where K : IComparable,IConvertible {...}
您可以為您的類使用的每個一般型別引數提供約束,例如:
public class LinkedList where K : IComparable where T : ICloneable {...}
您可以具有一個基類約束,這意味著規定一般型別引數派生自特定的基類:
public class MyBaseClass {...} public class LinkedList where K : MyBaseClass {...}
但是,在一個約束中最多隻能使用一個基類,這是因為 C# 不支援實現的多重繼承。顯然,您約束的基類不能是密封類或靜態類,並且由編譯器實施這一限制。此外,您不能將 System.Delegate 或 System.Array 約束為基類。
您可以同時約束一個基類以及一個或多個介面,但是該基類必須首先出現在派生約束列表中:
public class LinkedList where K : MyBaseClass, IComparable {...}
C# 確實允許您將另一個一般型別引數指定為約束:
public class MyClass where T : U {...}
在處理派生約束時,您可以通過使用基型別本身來滿足該約束,而不必非要使用它的嚴格子類。例如:
public interface IMyInterface {...} public class MyClass where T : IMyInterface {...} MyClass obj = new MyClass();
或者,您甚至可以:
public class MyOtherClass {...} public class MyClass where T : MyOtherClass {...} MyClass obj = new MyClass();
最後,請注意,在提供派生約束時,您約束的基型別(介面或基類)必須與您定義的一般型別引數具有一致的可見性。例如,以下約束是有效的,因為內部型別可以使用公共型別:
public class MyBaseClass {} internal class MySubClass where T : MyBaseClass {} 但是,如果這兩個類的可見性被顛倒,例如: internal class MyBaseClass {} public class MySubClass where T : MyBaseClass {}
則編譯器會發出錯誤,因為程式集外部的任何客戶端都無法使用一般型別 MySubClass,從而使得 MySubClass 實際上成為內部型別而不是公共型別。外部客戶端無法使用 MySubClass 的原因是,要宣告 MySubClass 型別的變數,它們需要使用派生自內部型別 MyBaseClass 的型別。
建構函式約束
假設您要在一般類的內部例項化一個新的一般物件。問題在於,C# 編譯器不知道客戶端將使用的型別實參是否具有匹配的建構函式,因而它將拒絕編譯例項化行。
為了解決該問題,C# 允許約束一般型別引數,以使其必須支援公共預設建構函式。這是使用 new() 約束完成的。例如,以下是一種實現程式碼塊 3 中的一般 Node 的預設建構函式的不同方式。
class Node where T : new() { public K Key; public T Item; public Node NextNode; public Node() { Key = default(K); Item = new T(); NextNode = null; } }
可以將建構函式約束與派生約束組合起來,前提是建構函式約束出現在約束列表中的最後:
public class LinkedList where K : IComparable,new() {...}
引用/值型別約束
可以使用 struct 約束將一般型別引數約束為值型別(例如,int、bool 和 enum),或任何自定義結構:
public class MyClass where T : struct {...}
同樣,可以使用 class 約束將一般型別引數約束為引用型別(類):
public class MyClass where T : class {...}
不能將引用/值型別約束與基類約束一起使用,因為基類約束涉及到類。同樣,不能使用結構和預設建構函式約束,因為預設建構函式約束也涉及到類。雖然您可以使用類和預設建構函式約束,但這樣做沒有任何價值。可以將引用/值型別約束與介面約束組合起來,前提是引用/值型別約束出現在約束列表的開頭。
返回頁首泛型和強制型別轉換
C# 編譯器只允許將一般型別引數隱式強制轉換到 Object 或約束指定的型別,如程式碼塊 5 所示。這樣的隱式強制型別轉換是型別安全的,因為可以在編譯時發現任何不相容性。
程式碼塊 5. 一般型別引數的隱式強制型別轉換
interface ISomeInterface {...} class BaseClass {...} class MyClass where T : BaseClass,ISomeInterface { void SomeMethod(T t) { ISomeInterface obj1 = t; BaseClass obj2 = t; object obj3 = t; } }
編譯器允許您將一般型別引數顯式強制轉換到其他任何介面,但不能將其轉換到類:
interface ISomeInterface {...} class SomeClass {...} class MyClass { void SomeMethod(T t) { ISomeInterface obj1 = (ISomeInterface)t;//Compiles SomeClass obj2 = (SomeClass)t; //Does not compile } }
但是,您可以使用臨時的 Object 變數,將一般型別引數強制轉換到其他任何型別:
class SomeClass {...} class MyClass { void SomeMethod(T t) { object temp = t; SomeClass obj = (SomeClass)temp; } }
不用說,這樣的顯式強制型別轉換是危險的,因為如果為取代一般型別引數而使用的型別實參不是派生自您要顯式強制轉換到的型別,則可能在執行時引發異常。要想不冒引發強制型別轉換異常的危險,一種更好的辦法是使用 is 和 as 運算子,如程式碼塊 6 所示。如果一般型別引數的型別是所查詢的型別,則 is 運算子返回 true;如果這些型別相容,則 as 將執行強制型別轉換,否則將返回 null。您可以對一般型別引數以及帶有特定型別實參的一般類使用 is 和 as。
程式碼塊 6. 對一般型別引數使用“is”和“as”運算子
public class MyClass { public void SomeMethod(T t) { if(t is int) {...} if(t is LinkedList) {...} string str = t as string; if(str != null) {...} LinkedList list = t as LinkedList; if(list != null) {...} } }返回頁首
繼承和泛型
在從一般基類派生時,必須提供型別實參,而不是該基類的一般型別引數:
public class BaseClass {...} public class SubClass : BaseClass {...}
如果子類是一般的而非具體的型別實參,則可以使用子類一般型別引數作為一般基類的指定型別:
public class SubClass : BaseClass {...}
在使用子類一般型別引數時,必須在子類級別重複在基類級別規定的任何約束。例如,派生約束:
public class BaseClass where T : ISomeInterface {...} public class SubClass : BaseClass where T : ISomeInterface {...}
或建構函式約束:
public class BaseClass where T : new() { public T SomeMethod() { return new T(); } } public class SubClass : BaseClass where T : new() {...}
基類可以定義其簽名使用一般型別引數的虛擬方法。在重寫它們時,子類必須在方法簽名中提供相應的型別:
public class BaseClass { public virtual T SomeMethod() {...} } public class SubClass: BaseClass<int> { public override int SomeMethod() {...} }
如果該子類是一般型別,則它還可以在重寫時使用它自己的一般型別引數:
public class SubClass: BaseClass { public override T SomeMethod() {...} }
您可以定義一般介面、一般抽象類,甚至一般抽象方法。這些型別的行為像其他任何一般基型別一樣:
public interface ISomeInterface { T SomeMethod(T t); } public abstract class BaseClass { public abstract T SomeMethod(T t); } public class SubClass : BaseClass { public override T SomeMethod(T t) {...) }
一般抽象方法和一般介面有一種有趣的用法。在 C# 2.0 中,不能對一般型別引數使用諸如 + 或 += 之類的運算子。例如,以下程式碼無法編譯,因為 C# 2.0 不具有運算子約束:
public class Calculator { public T Add(T arg1,T arg2) { return arg1 + arg2;//Does not compile } //Rest of the methods }
但是,您可以通過定義一般操作,使用抽象方法(最好使用介面)進行補償。由於抽象方法的內部不能具有任何程式碼,因此可以在基類級別指定一般操作,並且在子類級別提供具體的型別和實現:
public abstract class BaseCalculator { public abstract T Add(T arg1,T arg2); public abstract T Subtract(T arg1,T arg2); public abstract T Divide(T arg1,T arg2); public abstract T Multiply(T arg1,T arg2); } public class MyCalculator : BaseCalculator { public override int Add(int arg1, int arg2) { return arg1 + arg2; } //Rest of the methods }
一般介面還可以產生更加乾淨一些的解決方案:
public interface ICalculator { T Add(T arg1,T arg2); //Rest of the methods } public class MyCalculator : ICalculator { public int Add(int arg1, int arg2) { return arg1 + arg2; } //Rest of the methods }返回頁首
一般方法
在 C# 2.0 中,方法可以定義特定於其執行範圍的一般型別引數:
public class MyClass { public void MyMethod(X x) {...} }
這是一種重要的功能,因為它使您可以每次用不同的型別呼叫該方法,而這對於實用工具類非常方便。
即使包含類根本不使用泛型,您也可以定義方法特定的一般型別引數:
public class MyClass { public void MyMethod(T t) {...} }
該功能僅適用於方法。屬性或索引器只能使用在類的作用範圍中定義的一般型別引數。
在呼叫定義了一般型別引數的方法時,您可以提供要在呼叫場所使用的型別:
MyClass obj = new MyClass(); obj.MyMethod(3);
因此,當呼叫該方法時,C# 編譯器將足夠聰明,從而基於傳入的引數的型別推斷出正確的型別,並且它允許完全省略型別規範:
MyClass obj = new MyClass(); obj.MyMethod(3);
該功能稱為一般型別推理。請注意,編譯器無法只根據返回值的型別推斷出型別:
public class MyClass { public T MyMethod() {} } MyClass obj = new MyClass(); int number = obj.MyMethod();//Does not compile
當方法定義它自己的一般型別引數時,它還可以定義這些型別的約束:
public class MyClass { public void SomeMethod(T t) where T : IComparable {...} }
但是,您無法為類級別一般型別引數提供方法級別約束。類級別一般型別引數的所有約束都必須在類作用範圍中定義。
在重寫定義了一般型別引數的虛擬方法時,子類方法必須重新定義該方法特定的一般型別引數:
public class BaseClass { public virtual void SomeMethod(T t) {...} } public class SubClass : BaseClass { public override void SomeMethod(T t) {...} }
子類實現必須重複在基礎方法級別出現的所有約束:
public class BaseClass { public virtual void SomeMethod(T t) where T : new() {...} } public class SubClass : BaseClass { public override void SomeMethod(T t) where T : new() {...} }
請注意,方法重寫不能定義沒有在基礎方法中出現的新約束。
此外,如果子類方法呼叫虛擬方法的基類實現,則它必須指定要代替一般基礎方法型別引數使用的型別實參。您可以自己顯式指定它,或者依靠型別推理(如果可用):
public class BaseClass { public virtual void SomeMethod(T t) {...} } public class SubClass : BaseClass { public override void SomeMethod(T t) { base.SomeMethod(t); base.SomeMethod(t); } }
一般靜態方法
C# 允許定義使用一般型別引數的靜態方法。但是,在呼叫這樣的靜態方法時,您需要在呼叫場所為包含類提供具體的型別,如下面的示例所示:
public class MyClass { public static T SomeMethod(T t) {...} } int number = MyClass.SomeMethod(3);
靜態方法可以定義方法特定的一般型別引數和約束,就像例項方法一樣。在呼叫這樣的方法時,您需要在呼叫場所提供方法特定的型別 — 可以按如下方式顯式提供:
public class MyClass { public static T SomeMethod(T t,X x) {..} } int number = MyClass.SomeMethod(3,"AAA");
或者依靠型別推理(如果可能):
int number = MyClass.SomeMethod(3,"AAA");
一般靜態方法遵守施加於它們在類級別使用的一般型別引數的所有約束。就像例項方法一樣,您可以為由靜態方法定義的一般型別引數提供約束:
public class MyClass { public static T SomeMethod(T t) where T : IComparable {...} }
C# 中的運算子只是靜態方法而已,並且 C# 允許您為自己的一般型別過載運算子。假設程式碼塊 3 的一般 LinkedList 提供了用於串聯連結串列的 + 運算子。+ 運算子使您能夠編寫下面這段優美的程式碼:
LinkedList list1 = new LinkedList(); LinkedList list2 = new LinkedList(); ... LinkedList list3 = list1+list2;
程式碼塊 7 顯示 LinkedList 類上的一般 + 運算子的實現。請注意,運算子不能定義新的一般型別引數。
程式碼塊 7. 實現一般運算子
public class LinkedList { public static LinkedList operator+(LinkedList lhs, LinkedList rhs) { return concatenate(lhs,rhs); } static LinkedList concatenate(LinkedList list1, LinkedList list2) { LinkedList newList = new LinkedList(); Node current; current = list1.m_Head; while(current != null) { newList.AddHead(current.Key,current.Item); current = current.NextNode; } current = list2.m_Head; while(current != null) { newList.AddHead(current.Key,current.Item); current = current.NextNode; } return newList; } //Rest of LinkedList }返回頁首
一般委託
在某個類中定義的委託可以利用該類的一般型別引數。例如:
public class MyClass { public delegate void GenericDelegate(T t); public void SomeMethod(T t) {...} }
在為包含類指定型別時,也會影響到委託:
MyClass obj = new MyClass(); MyClass.GenericDelegate del; del = new MyClass.GenericDelegate(obj.SomeMethod); del(3);
C# 2.0 使您可以將方法引用的直接分配轉變為委託變數:
MyClass obj = new MyClass(); MyClass.GenericDelegate del; del = obj.SomeMethod;
我將把該功能稱為委託推理。編譯器能夠推斷出您分配到其中的委託的型別,查明目標物件是否具有采用您指定的名稱的方法,並且驗證該方法的簽名匹配。然後,編譯器建立所推斷出的引數型別(包括正確的型別而不是一般型別引數)的新委託,並且將新委託分配到推斷出的委託中。
像類、結構和方法一樣,委託也可以定義一般型別引數:
public class MyClass { public delegate void GenericDelegate(T t,X x); }
在類的作用範圍外部定義的委託可以使用一般型別引數。在該情況下,在宣告和例項化委託時,必須為其提供型別實參:
public delegate void GenericDelegate(T t); public class MyClass { public void SomeMethod(int number) {...} } MyClass obj = new MyClass(); GenericDelegate del; del = new GenericDelegate(obj.SomeMethod); del(3);
另外,還可以在分配委託時使用委託推理:
MyClass obj = new MyClass(); GenericDelegate del; del = obj.SomeMethod;
當然,委託可以定義約束以伴隨它的一般型別引數:
public delegate void MyDelegate(T t) where T : IComparable;
委託級別約束只在使用端實施(在宣告委託變數和例項化委託物件時),類似於在型別或方法的作用範圍中實施的其他任何約束。
一般委託對於事件尤其有用。您可以精確地定義一組有限的一般委託(只按照它們需要的一般型別引數的數量進行區分),並且使用這些委託來滿足所有事件處理需要。程式碼塊 8 演示了一般委託和一般事件處理方法的用法。
程式碼塊 8. 一般事件處理
public delegate void GenericEventHandler (S sender,A args); public class MyPublisher { public event GenericEventHandler MyEvent; public void FireEvent() { MyEvent(this,EventArgs.Empty); } } public class MySubscriber //Optional: can be a specific type { public void SomeMethod(MyPublisher sender,A args) {...} } MyPublisher publisher = new MyPublisher(); MySubscriber subscriber = new MySubscriber(); publisher.MyEvent += subscriber.SomeMethod;
程式碼塊 8 使用名為 GenericEventHandler 的一般委託,它接受一般傳送者型別和一般型別引數。顯然,如果您需要更多的引數,則可以簡單地新增更多的一般型別引數,但是我希望模仿按如下方式定義的 .NET EventHandler 來設計 GenericEventHandler:
public void delegate EventHandler(object sender,EventArgs args);
與 EventHandler 不同,GenericEventHandler 是型別安全的(如程式碼塊 8 所示),因為它只接受 MyPublisher 型別的物件(而不是純粹的 Object)作為傳送者。實際上,.NET 已經在 System 名稱空間中定義了一般樣式的 EventHandler:
public void delegate EventHandler(object sender,A args) where A : EventArgs;返回頁首
泛型和反射
在 .NET 2.0 中,擴充套件了反射以支援一般型別引數。型別 Type 現在可以表示帶有特定型別實參(稱為繫結型別)或未指定(未繫結)型別的一般型別。像 C# 1.1 中一樣,您可以通過使用 typeof 運算子或者通過呼叫每個型別支援的 GetType() 方法來獲得任何型別的 Type。不管您選擇哪種方式,都會產生相同的 Type。例如,在以下程式碼示例中,type1 與 type2 完全相同。
LinkedList list = new LinkedList(); Type type1 = typeof(LinkedList); Type type2 = list.GetType(); Debug.Assert(type1 == type2);
typeof 和 GetType() 都可以對一般型別引數進行操作:
public class MyClass { public void SomeMethod(T t) { Type type = typeof(T); Debug.Assert(type == t.GetType()); } }
此外,typeof 運算子還可以對未繫結的一般型別進行操作。例如:
public class MyClass {} Type unboundedType = typeof(MyClass<>); Trace.WriteLine(unboundedType.ToString()); //Writes: MyClass`1[T]
所追蹤的數字 1 是所使用的一般型別的一般型別引數的數量。請注意空 <> 的用法。要對帶有多個型別引數的未繫結一般型別進行操作,請在 <> 中使用“,”:
public class LinkedList {...} Type unboundedList = typeof(LinkedList<,>); Trace.WriteLine(unboundedList.ToString()); //Writes: LinkedList`2[K,T]
Type 具有新的方法和屬性,用於提供有關該型別的一般方面的反射資訊。程式碼塊 9 顯示了新方法。
程式碼塊 9. Type 的一般反射成員
public abstract class Type : //Base types { public virtual bool ContainsGenericParameters{get;} public virtual int GenericParameterPosition{get;} public virtual bool HasGenericArguments{get;} public virtual bool IsGenericParameter{get;} public virtual bool IsGenericTypeDefinition{get;} public virtual Type BindGenericParameters(Type[] typeArgs); public virtual Type[] GetGenericArguments(); public virtual Type GetGenericTypeDefinition(); //Rest of the members }
上述新成員中最有用的是 HasGenericArguments 屬性,以及 GetGenericArguments() 和 GetGenericTypeDefinition() 方法。Type 的其餘新成員用於高階的且有點深奧的方案,這些方案超出了本文的範圍。
正如它的名稱所指示的那樣,如果由 Type 物件表示的型別使用一般型別引數,則 HasGenericArguments 被設定為 true。GetGenericArguments() 返回與所使用的型別引數相對應的 Type 陣列。GetGenericTypeDefinition() 返回一個表示基礎型別的一般形式的 Type。程式碼塊 10 演示如何使用上述一般處理 Type 成員獲得有關程式碼塊 3 中的 LinkedList 的一般反射資訊。
程式碼塊 10. 使用 Type 進行一般反射
LinkedList list = new LinkedList(); Type boundedType = list.GetType(); Trace.WriteLine(boundedType.ToString()); //Writes: LinkedList`2[System.Int32,System.String] Debug.Assert(boundedType.HasGenericArguments); Type[] parameters = boundedType.GetGenericArguments(); Debug.Assert(parameters.Length == 2); Debug.Assert(parameters[0] == typeof(int)); Debug.Assert(parameters[1] == typeof(string)); Type unboundedType = boundedType.GetGenericTypeDefinition(); Debug.Assert(unboundedType == typeof(LinkedList<,>)); Trace.WriteLine(unboundedType.ToString()); //Writes: LinkedList`2[K,T]
與 Type 類似,MethodInfo 和它的基類 MethodBase 具有反射一般方法資訊的新成員。
與 C# 1.1 中一樣,您可以使用 MethodInfo(以及很多其他選項)進行晚期繫結呼叫。但是,您為晚期繫結傳遞的引數的型別,必須與取代一般型別引數而使用的繫結型別(如果有)相匹配:
LinkedList list = new LinkedList(); Type type = list.GetType(); MethodInfo methodInfo = type.GetMethod("AddHead"); object[] args = {1,"AAA"}; methodInfo.Invoke(list,args);
屬性和泛型
在定義屬性時,可以使用列舉 AttributeTargets 的新 GenericParameter 值,通知編譯器屬性應當以一般型別引數為目標:
[AttributeUsage(AttributeTargets.GenericParameter)] public class SomeAttribute : Attribute {...}
請注意,C# 2.0 不允許定義一般屬性。
//Does not compile: public class SomeAttribute : Attribute {...}
然而,屬性類可以通過使用一般型別或者定義 Helper 一般方法(像其他任何型別一樣)在內部利用泛型:
public class SomeAttribute : Attribute { void SomeMethod(T t) {...} LinkedList m_List = new LinkedList(); }返回頁首
泛型和 .NET Framework
為了對本文做一下小結,下面介紹 .NET 中除 C# 本身以外的其他一些領域如何利用泛型或者與泛型互動。
System.Array 和泛型
System.Array 型別通過很多一般靜態方法進行了擴充套件。這些一般靜態方法專門用於自動執行和簡化處理陣列的常見任務,例如,遍歷陣列並且對每個元素執行操作、掃描陣列,以查詢匹配某個條件(謂詞)的值、對陣列進行變換和排序等等。程式碼塊 11 是這些靜態方法的部分清單。
程式碼塊 11. System.Array 的一般方法
public abstract class Array { //Partial listing of the static methods: public static IList AsReadOnly(T[] array); public static int BinarySearch(T[] array, T value); public static int BinarySearch(T[] array, T value, IComparer comparer); public static U[] ConvertAll(T[] array, Converter converter); public static bool Exists(T[] array,Predicate match); public static T Find(T[] array,Predicate match); public static T[] FindAll(T[] array, Predicate match); public static int FindIndex(T[] array, Predicate match); public static void ForEach(T[] array, Action action); public static int IndexOf(T[] array, T value); public static void Sort(K[] keys, V[] items, IComparer comparer); public static void Sort(T[] array,Comparison comparison) }
System.Array 的靜態一般方法都使用 System 名稱空間中定義的下列四個一般委託:
public delegate void Action(T t); public delegate int Comparison(T x, T y); public delegate U Converter(T from); public delegate bool Predicate(T t);
程式碼塊 12 演示如何使用這些一般方法和委託。它用從 1 到 20 的所有整數初始化一個數組。然後,程式碼通過一個匿名方法和 Action 委託,使用 Array.ForEach() 方法來跟蹤這些數字。使用第二個匿名方法和 Predicate 委託,程式碼通過呼叫 Array.FindAll() 方法(它返回另一個相同的一般型別的陣列),來查詢該陣列中的所有質數。最後,使用相同的 Action 委託和匿名方法來跟蹤這些質數。請注意程式碼塊 12 中型別引數推理的用法。您在使用靜態方法時無須指定型別引數。
程式碼塊 12. 使用 System.Array 的一般方法
int[] numbers = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20}; Action trace = delegate(int number) { Trace.WriteLine(number); }; Predicate isPrime = delegate(int number) { switch(number) { case 1:case 2:case 3:case 5:case 7: case 11:case 13:case 17:case 19: return true; default: return false; } }; Array.ForEach(numbers,trace); int[] primes = Array.FindAll(numbers,isPrime); Array.ForEach(primes,trace);
在 System.Collections.Generic 名稱空間中定義的類 List 中,也可以得到類似的一般方法。這些方法使用四個相同的一般委託。實際上,您還可以在您的程式碼中利用這些委託,如以下部分所示。
靜態集合類
儘管 System.Array 和 List 都提供了能夠大大簡化自身使用方式的、方便的實用工具方法,但 .NET 沒有為其他集合提供這樣的支援。為了對此進行補償,本文隨附的原始碼包含了靜態 Helper 類 Collection,其定義如下所示:
public static class Collection { public static IList AsReadOnly(IEnumerable collection); public static U[] ConvertAll(IEnumerable collection, Converter converter); public static bool Contains(IEnumerable collection,T item) where T : IComparable; public static bool Exists(IEnumerable collection,Predicate); public static T Find(IEnumerable collection,Predicate match); public static T[] FindAll(IEnumerable collection, Predicate match); public static int FindIndex(IEnumerable collection,T value) where T : IComparable; public static T FindLast(IEnumerable collection, Predicate match); public static int FindLastIndex(IEnumerable collection,T value) where T : IComparable; public static void ForEach(IEnumerable collection,Action action); public static T[] Reverse(IEnumerable collection); public static T[] Sort(IEnumerable collection); public static T[] ToArray(IEnumerable collection); public static bool TrueForAll(IEnumerable collection, Predicate match); //Overloaded versions for IEnumerator }
Collection 的實現簡單易懂。例如,以下為 ForEach() 方法:
public static void ForEach(IEnumerator iterator,Action action) { /* Some parameter checking here, then: */ while(iterator.MoveNext()) { action(iterator.Current); } }
Collection 靜態類的用法非常類似於 Array 和 List,它們都利用相同的一般委託。您可以將 Collection 用於任何集合,只要該集合支援 IEnumerable 或 IEnumerator:
Queue queue = new Queue(); //Some code to initialize queue Action trace = delegate(int number) { Trace.WriteLine(number); }; Collection.ForEach(queue,trace);
一般集合
System.Collections 中的資料結構全部都是基於 Object 的,因而繼承了本文開頭描述的兩個問題,即效能較差和缺少型別安全。.NET 2.0 在 System.Collections.Generic 名稱空間中引入了一組一般集合。例如,有一般的 Stack 類和一般的 Queue 類。Dictionary 資料結構等效於非一般的 HashTable,並且還有一個有點像 SortedList 的 SortedDictionary 類。類 List 類似於非一般的 ArrayList。表 1 將 System.Collections.Generic 的主要型別對映到 System.Collections 中的那些主要型別。
表 1. 將 System.Collections.Generic 對映到 System.Collections | |
System.Collections.Generic | System.Collections |
Comparer |
Comparer |
Dictionary |
HashTable |
LinkedList |
- |
List |
ArrayList |
Queue |
Queue |
SortedDictionary |
SortedList |
Stack |
Stack |
ICollection |
ICollection |
IComparable |
System.IComparable |
IDictionary |
IDictionary |
IEnumerable |
IEnumerable |
IEnumerator |
IEnumerator |
IList |
IList |
System.Collections.Generic 中的所有一般集合還實現了一般的 IEnumerable 介面,該介面的定義如下所示:
public interface IEnumerable { IEnumerator GetEnumerator(); } public interface IEnumerator : IDisposable { T Current{get;} bool MoveNext(); }
簡單說來,IEnumerable 提供了對 IEnumerator 迭代器介面的訪問,該介面用於對集合進行抽象迭代。所有集合都在巢狀結構上實現了 IEnumerable,其中,一般型別引數 T 是集合儲存的型別。
特別有趣的是,詞典集合定義它們的迭代器的方式。詞典實際上是兩個型別(而非一個型別)的一般引數(鍵和值)集合。System.Collection.Generic 提供了一個名為 KeyValuePair 的一般結構,其定義如下所示:
struct KeyValuePair { public KeyValuePair(K key,V value); public K Key(get;set;) public V Value(get;set;) }
KeyValuePair 簡單地儲存一般鍵和一般值組成的對。該結構就是詞典作為集合進行管理的型別,並且是它用於實現它的 IEnumerable 的型別。Dictionary 類將一般 KeyValuePair 結構指定為 IEnumerable 和 ICollection 的項引數:
public class Dictionary : IEnumerable<KEYVALUEPAIR>, ICollection<KEYVALUEPAIR>, //More interfaces {...}