Juval Lowy
IDesign
摘要:本文討論泛型處理的問題空間、它們的實現方式、該編程模型的好處,以及獨特的創新(例如,約束、一般方法和委託以及一般繼承)。此外,本文還討論 .NET Framework 如何利用泛型。
下載 GenericsInCSharp.msi 示例文件 。
注 本文假定讀者熟悉 C# 1.1。有關 C# 語言的詳細信息,請訪問 http://msdn.microsoft.com/vcsharp/language。
本頁內容
簡介
泛型問題陳述
什麼是泛型
應用泛型
一般約束
泛型和強制類型轉換
繼承和泛型
一般方法
一般委託
泛型和反射
泛型和 .NET Framework
小結
簡介
泛型是 C# 2.0 的最強大的功能。通過泛型可以定義類型安全的數據結構,而無須使用實際的數據類型。這能夠顯著提高性能並得到更高質量的代碼,因爲您可以重用數據處理算法,而無須複製類型特定的代碼。在概念上,泛型類似於 C++ 模板,但是在實現和功能方面存在明顯差異。本文討論泛型處理的問題空間、它們的實現方式、該編程模型的好處,以及獨特的創新(例如,約束、一般方法和委託以及一般繼承)。您還將瞭解在 .NET Framework 的其他領域(例如,反射、數組、集合、序列化和遠程處理)中如何利用泛型,以及如何在所提供的基本功能的基礎上進行改進。
泛型問題陳述
考慮一種普通的、提供傳統 Push() 和 Pop() 方法的數據結構(例如,堆棧)。在開發通用堆棧時,您可能願意使用它來存儲各種類型的實例。在 C# 1.1 下,您必須使用基於 Object 的堆棧,這意味着,在該堆棧中使用的內部數據類型是難以歸類的 Object,並且堆棧方法與 Object 交互:
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 的解決方案的第二個問題(通常更爲嚴重)是類型安全。因爲編譯器允許在任何類型和 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 的數據結構。結果,大多數 C# 1.1 開發人員發現類型特定的數據結構不實用,並且選擇使用基於 Object 的數據結構,儘管它們存在缺點。
什麼是泛型
通過泛型可以定義類型安全類,而不會損害類型安全、性能或工作效率。您只須一次性地將服務器實現爲一般服務器,同時可以用任何類型來聲明和使用它。爲此,需要使用 < 和 > 括號,以便將一般類型參數括起來。例如,可以按如下方式定義和使用一般堆棧:
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 爲類型實參。
該編程模型的優點在於,內部算法和數據操作保持不變,而實際數據類型可以基於客戶端使用服務器代碼的方式進行更改。
泛型實現
表面上,C# 泛型的語法看起來與 C++ 模板類似,但是編譯器實現和支持它們的方式存在重要差異。正如您將在後文中看到的那樣,這對於泛型的使用方式具有重大意義。
注 在本文中,當提到 C++ 時,指的是傳統 C++,而不是帶有託管擴展的 Microsoft 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 {...}
KeyValuePair 中使用的鍵和值類型參數當然是詞典自己的一般鍵和值類型參數。您無疑可以在您自己的使用鍵和值對的一般數據結構中完成同樣的工作。例如:
public class LinkedList : IEnumerable<KEYVALUEPAIR> where K : IComparable {...}
序列化和泛型
.NET 允許您具有可序列化的一般類型:
[Serializable] public class MyClass {...}
在序列化類型時,除了持久保持對象成員的狀態以外,.NET 還持久保持有關該對象及其類型的元數據。如果可序列化的類型是一般類型並且它包含綁定類型,則有關該一般類型的元數據還包含有關該綁定類型的類型信息。因此,一般類型的每個帶有特定參數類型的變形都被視爲唯一的類型。例如,您不能將對象類型 MyClass 序列化(而只能將其反序列化)爲 MyClass 類型的對象。序列化一般類型的實例與序列化非一般類型沒有什麼不同。但是,在反序列化該類型時,您需要通過匹配的特定類型聲明變量,並且在向下強制轉換從 Deserialize 返回的 Object 時再次指定這些類型。代碼塊 13 顯示了一般類型的序列化和反序列化。
代碼塊 13. 一般類型的客戶端序列化
[Serializable] public class MyClass {...} MyClass obj1 = new MyClass(); IFormatter formatter = new BinaryFormatter();? Stream stream = new FileStream("obj.bin",FileMode.Create,FileAccess.ReadWrite); using(stream) { formatter.Serialize(stream,obj1); stream.Seek(0,SeekOrigin.Begin); MyClass obj2; obj2 = (MyClass)formatter.Deserialize(stream); }
請注意,IFormatter 是基於對象的。您可以通過定義 IFormatter 的一般版本進行補償:
public interface IGenericFormatter { T Deserialize(Stream serializationStream); void Serialize(Stream serializationStream,T graph); }
您可以通過包含一個基於對象的格式化程序來實現 IGenericFormatter:
public class GenericFormatter : IGenericFormatter where F : IFormatter,new() { IFormatter m_Formatter = new F(); public T Deserialize(Stream serializationStream) { return (T)m_Formatter.Deserialize(serializationStream); } public void Serialize(Stream serializationStream,T graph) { m_Formatter.Serialize(serializationStream,graph); } }
請注意一般類型參數 F 上的兩個約束的用法。儘管可以原樣使用 GenericFormatter <F>:
using GenericBinaryFormatter = GenericFormatter; using GenericSoapFormatter2 = GenericFormatter;
但是,您還可以將該格式化程序強類型化以使用:
public sealed class GenericBinaryFormatter : GenericFormatter {} public sealed class GenericSoapFormatter : GenericFormatter {}
強類型化定義的優點是可以跨文件和程序集共享它,這與 using 別名相反。
代碼塊 14 與代碼塊 13 相同,唯一的不同之處在於它使用一般格式化程序:
代碼塊 14. 使用 IGenericFormatter
[Serializable] public class MyClass {...} MyClass obj1 = new MyClass(); IGenericFormatter formatter = new GenericBinaryFormatter();? Stream stream = new FileStream("obj.bin",FileMode.Create,FileAccess.ReadWrite); using(stream) { formatter.Serialize(stream,obj1); stream.Seek(0,SeekOrigin.Begin); MyClass obj2; obj2 = formatter.Deserialize(stream); }
泛型和遠程處理
可以定義和部署利用泛型的遠程類,並且可以使用編程或管理配置。請考慮使用泛型並且派生自 MarshalByRefObject 的類 MyServer。
public class MyServer : MarshalByRefObject {...}
只有當類型參數 T 是可封送的對象時,您才能通過遠程處理訪問該類。這意味着 T 是可序列化的類型或者派生自 MarshalByRefObject。您可以通過將 T 約束爲派生自 MarshalByRefObject 來實施這一要求。
public class MyServer : MarshalByRefObject where T : MarshalByRefObject {...}
在使用管理類型註冊時,您需要指定要取代一般類型參數而使用的確切類型實參。您必須以與語言無關的方式命名這些類型,並且提供完全限定命名空間。例如,假設類 MyServer 在命名空間 RemoteServer 中的程序集 ServerAssembly 中定義,並且您希望在客戶端激活模式下將其與整型而不是一般類型參數 T 一起使用。在該情況下,配置文件中必需的客戶端類型註冊條目應該是:
<client url="...some url goes here..."> <activated type="RemoteServer.MyServer<b>[[System.Int32]]</b>,ServerAssembly"/> </client>
配置文件中的匹配主機端類型註冊條目是:
<service> <activated type="RemoteServer.MyServer<b>[[System.Int32]]</b>,ServerAssembly"/> </service>
雙方括號用來指定多個類型。例如:
LinkedList[[System.Int32],[System.String]]
在使用編程配置時,您可以用類似於 C# 1.1 的方式配置激活模式和類型註冊,不同之處在於,當定義遠程對象的類型時,您必須提供類型實參而不是一般類型參數。例如,對於主機端激活模式和類型註冊,您可以編寫如下代碼:
Type serverType = typeof(MyServer); RemotingConfiguration.RegisterActivatedServiceType(serverType);
對於客戶端類型激活模式和位置註冊,具有以下代碼:
Type serverType = typeof(MyServer); string url = ...; //some url initialization RemotingConfiguration.RegisterWellKnownClientType(serverType,url);
當實例化遠程服務器時,只須提供類型參數,就好像您在使用本地一般類型一樣:
MyServer obj; obj = new MyServer(); //Use obj
除了使用 new 以外,客戶端還可以選擇使用 Activator 類的方法來連接到遠程對象。在使用 Activator.GetObject() 時,您需要提供要使用的類型實參,並且在顯式強制轉換返回的 Object 時提供實參類型:
string url = ...; //some url initialization Type serverType = typeof(MyServer); MyServer obj; obj = (MyServer)Activator.GetObject(serverType,url); //Use obj
您還可以將 Activator.CreateInstance() 與一般類型一起使用:
Type serverType = typeof(MyServer); MyServer obj; obj = (MyServer)Activator.CreateInstance(serverType); //Use obj
實際上,Activator 還提供 CreateInstance() 的一般版本,定義如下:
T CreateInstance<T>();
CreateInstance() 的使用方式類似於非一般方法,只是添加了類型安全的好處:
Type serverType = typeof(MyServer); MyServer obj; obj = Activator.CreateInstance(serverType); //Use obj
泛型無法完成的工作
在 .NET 2.0 下,您不能定義一般 Web 服務,即使用一般類型參數的 Web 方法。原因是沒有哪個 Web 服務標準支持一般服務。
您還不能在服務組件上使用一般類型。原因是泛型不能滿足 COM 可見性要求,而該要求對於服務組件而言是必需的(就像您無法在 COM 或 COM+ 中使用 C++ 模板一樣)。
小結
C# 泛型是開發工具庫中的一個無價之寶。它們可以提高性能、類型安全和質量,減少重複性的編程任務,簡化總體編程模型,而這一切都是通過優雅的、可讀性強的語法完成的。儘管 C# 泛型的根基是 C++ 模板,但 C# 通過提供編譯時安全和支持將泛型提高到了一個新水平。C# 利用了兩階段編譯、元數據以及諸如約束和一般方法之類的創新性的概念。毫無疑問,C# 的將來版本將繼續發展泛型,以便添加新的功能,並且將泛型擴展到諸如數據訪問或本地化之類的其他 .NET Framework 領域。
Juval Lowy 是 IDesign(一家諮詢和培訓公司)的軟件架構師和負責人。Juval 是 Microsoft 的 Silicon Valley 地區主管,他與 Microsoft 一起來幫助整個行業採用 .NET。他的最新著作是 Programming .NET Components 2nd Edition (O'Reilly, 2005)。該書專門論述面向組件的編程和設計以及相關的系統問題。Juval 參加了 Microsoft 對 .NET 的將來版本進行的內部設計審查,並且頻繁主持開發會議。Microsoft 將 Juval 視爲軟件傳奇人物以及全球頂尖 .NET 專家與行業領導人之一。您可以通過 http://www.idesign.net 與 Juval 聯繫。