簡單介紹CLR泛型及其優勢
泛型是對 CLR 類型系統的擴展,它允許開發人員定義那些未指定某些細節的類型。相反,當用戶代碼引用該代碼時,就會指定這些細節。引用泛型的代碼填充缺少的細節,并根據其特定需求對類型進行調整。泛型的命名反映了該功能的目標:允許在編寫代碼時不指定可能限制其使用范圍的細節。代碼本身就是泛型。稍后,我會對它進行更詳細的介紹。
CLR泛型預覽
正如使用任何新技術一樣,明白它的好處所在會有所幫助。那些熟悉 C++ 模板的用戶將會發現,泛型在托管代碼中具有相似的用途。但是,我不愿意對 CLR 泛型和 C++ 模板進行過多比較,因為泛型具有一些額外的好處,它不存在以下兩個常見問題:代碼臃腫和開發人員混淆。
CLR 泛型具有一些好處,如編譯時類型安全、二進制代碼重用、性能和清晰性。我將簡要介紹這些好處,您在閱讀本專欄的其余文章時,會更詳細地了解它們。例如,假設有兩個集合類:SortedList(Object 引用的集合)和 GenericSortedList< T>(任意類型的集合)。
類型安全:當用戶向 SortedList 類型的集合內添加 String 時,String 會隱式強制轉換為 Object。同樣,如果從該列表中檢索 String 對象,則它必須在運行時從 Object 引用強制轉換到 String 引用。這會造成編譯時缺少類型安全,從而使開發人員感到厭煩,并且易于出錯。相反,如果使用 GenericSortedList< String>(T 的類型被設置為 String),就會使所有的添加和查找方法使用 String 引用。這允許在編譯時(而非運行時)指定和檢查元素的類型。
二進制代碼重用:為了進行維護,開發人員可以選擇使用 SortedList ,通過從它派生 SortedListOfString 來實現編譯時的類型安全。此方法有一個問題,那就是必須對于每個需要類型安全列表的類型都編寫新代碼,而這會很快變成非常費力的工作。使用 GenericSortedList< T>,需要執行的全部操作就是將具有所需元素類型的類型實例化為 T。泛型代碼還有一個附加價值,那就是它在運行時生成,因此,對于無關元素類型的兩個擴展(如 GenericSortedList< String> 和 GenericSortedList< FileStream>)能夠重新使用同一個實時 (JIT) 編譯代碼的大部分。CLR 只是處理細節 — 代碼不再臃腫!
性能:關鍵在于:如果類型檢查在編譯時間進行,而不是在運行時間進行,則性能增強。在托管代碼中,引用和值之間的強制轉換既會導致裝箱又會導致取消裝箱,而且避免這樣的強制轉換可能會對性能產生同樣的負面影響。最近針對一個由一百萬個整數組成的數組進行了快速排序法基準測試,結果表明泛型方法比非泛型方法快三倍。這是由于完全避免了對這些值進行裝箱。如果針對由字符串引用組成的數組進行同樣的排序,則由于無需在運行時執行類型檢查,因此使用泛型方法后性能提高了 20%。
清晰性:泛型的清晰性體現在許多方面。約束是泛型的一個功能,它們會禁止對泛型代碼進行不兼容的擴展;使用泛型,您將不再面臨那些困擾 C++ 模板用戶的含混不清的編譯器錯誤。在 GenericSortedList< T> 示例中,集合類將有一個約束,該約束使集合類只處理可進行比較并依此進行排序的 T 類型。同樣,通常可以使用名為類型推理的功能來調用泛型方法,而無需使用任何特殊語法。當然,編譯時類型安全可以使應用程序代碼更加清晰。 我將在本文中詳細介紹約束、類型推理和類型安全。
#p#
一個簡單的CLR泛型示例
Whidbey CLR 版本將通過類庫中的一套泛型集合類來提供這些現成的好處。但是,可通過為應用程序定義其自己的泛型代碼,使其進一步受益于泛型。為了解釋這是如何完成的,我將首先修改一個簡單的鏈接列表節點類,使其成為泛型類類型。
以下代碼中的 Node 類只是包括一些基本內容。它有兩個字段:m_data(引用節點的數據)和 m_data(引用鏈接列表中的下一項)。這兩個字段都是由構造函數方法設置的。確實只有兩個其他點綴性功能,第一個功能是通過名為 Data 和 Next 的只讀屬性訪問 m_data 和 m_next 字段。第二個功能是對 System.Object 的 ToString 虛擬方法進行重寫。
- using System;
- // Definition of a node type for creating a linked list
- class Node {
- Object m_data;
- Node m_next;
- public Node(Object data, Node next) {
- m_data = data;
- m_next = next;
- }
- // Access the data for the node
- public Object Data {
- get { return m_data; }
- }
- // Access the next node
- public Node Next {
- get { return m_next; }
- }
- // Get a string representation of the node
- public override String ToString() {
- return m_data.ToString();
- }
- }
- // Code that uses the node type
- class App {
- public static void Main() {
- // Create a linked list of integers
- Node head = new Node(5, null);
- head = new Node(10, head);
- head = new Node(15, head);
- // Sum-up integers by traversing linked list
- Int32 sum = 0;
- for (Node current = head; current != null;
- current = current.Next) {
- sum += (Int32) current.Data;
- }
- // Output sum
- Console.WriteLine("Sum of nodes = {0}", sum);
- }
- }
上面還顯示了使用 Node 類的代碼。該引用代碼會受到某些限制。問題在于,為了能在許多上下文中使用,其數據必須為最基本的類型,即 System.Object。這意味著使用 Node 時,就會失去任何形式的編譯時類型安全。使用 Object 意味著算法或數據結構中的“任意類型”會強迫所使用的代碼在 Object 引用和實際數據類型之間進行強制轉換。應用程序中的任何類型不匹配錯誤只有在運行之后才被捕獲。如果在運行時嘗試進行強制轉換,這些錯誤會采用 InvalidCastException 形式。
此外,如果要向 Object 引用賦予任何基元值(如 Int32),則需要對實例進行裝箱。裝箱涉及到內存分配和內存復制,以及最后對已裝箱值進行的垃圾回收。最后,正如可在 圖 1 中看到的那樣,從 Object 引用強制轉換為值類型(如 Int32)會導致取消裝箱(也包括類型檢查)。 由于裝箱和取消裝箱會損害該算法的整體性能,因此您會明白為什么用 Object 就意味著“任何類型”都具有一定的缺點。
使用泛型重寫 Node 是解決這些問題的完美方法。讓我們看一下下面的代碼,您將發現 Node 類型被重寫為 Node< T> 類型。具有泛型行為的類型(如 Node< T>)是參數化類型,并且可被稱作 Parameterized Node、Node of T 或泛型Node。稍后我將介紹這個新的 C# 語法;讓我們首先深入研究一下 Node< T> 與 Node 有何不同。
- class Node
{ - T m_data;
- Node
m_next; - public Node(T data, Node
next) { - m_data = data;
- m_next = next;
- }
- // Access the data for the node
- public T Data {
- get { return m_data; }
- set { m_data = value; }
- }
- // Access the next node
- public Node
Next { - get { return m_next; }
- set { m_next = value; }
- }
- // Get a string representation of the node
- public override String ToString() {
- return m_data.ToString();
- }
- }
Node< T> 類型與 Node 類型在功能和結構上相似。二者均支持為任何給定類型的數據構建鏈接列表。但是,Node 使用 System.Object 來表示“任意類型”,而 Node< T> 不指定該類型。相反,Node< T> 使用名為 T 且作為類型占位符的類型參數。當使用者代碼使用 Node< T> 時,名為 T 的類型參數最終由 Node< T> 的參數來指定。
- class App {
- public static void Main() {
- // Create a linked list of integers
- Node
head = new Node(5, null);- head = new Node
(10, head); - head = new Node
(15, head); - // Sum up integers by traversing linked list
- Int32 sum = 0;
- for (Node
current = head; current != null;- current = current.Next) {
- sum += current.Data;
- }
- // Output sum
- Console.WriteLine("Sum of nodes = {0}", sum.ToString());
- }
- }
以上中的代碼使用了具有 32 位帶符號整數的 Node< T>,這是通過構造類似類型名稱:Node< Int32> 來實現的。在本例中,Int32 是類型參數 T 的類型變量。(順便說一句,C# 還將接受Node< int>,以便將 T 指示為 Int32。) 如果該代碼需要某種其他類型(如 String 引用)的鏈接列表,則這可通過將它指定為 T 的類型變量來完成,例如:Node< String>。
Node< T> 的好處在于:它的算法行為可被明確定義,而它所操作的數據類型仍保持未指定狀態。因此,Node< T> 類型在工作方式上是具體的;而泛型在所處理的內容方面又是具體的。總之,諸如鏈接列表應當擁有的數據類型等細節最好留給使用 Node< T> 的代碼來指定。
在討論泛型時,最好先明確兩種角色:定義代碼和引用代碼。定義代碼包括既聲明泛型代碼存在又定義類型成員(如方法和字段)的代碼。 圖 2 中顯示的是類型 Node< T> 的定義代碼。引用代碼是用戶代碼,它使用預定義的泛型代碼,并且該代碼還可以內置到另一個程序集中。 圖 3 是 Node< T> 的引用代碼示例。
考慮定義代碼和引用代碼非常有用,原因在于這兩種角色都在實際的可使用泛型代碼構造中起著一定的作用。 圖 3 中的引用代碼使用 Node< T> 來構造一個名為 Node< T> 的新類型。Node< Int32> 是一個截然不同的類型,它由以下兩個關鍵成分構建而成:Node< T>(由定義代碼創建),參數 T 的類型變量 Int32(由引用代碼指定)。只有使用這兩個成分才能使泛型代碼變得完整。
請注意,從面向對象的派生角度看,泛型類型(如 Node< T>)以及從泛型類型構造的類型(如 Node< Int32> 或 Node< String>)并不是相關類型。類型 Node< Int32>、Node< String> 和 Node< T> 類型是同輩,它們都是從 System.Object 直接派生而來。
#p#
C# Generic 語法
CLR 支持多種編程語言,因此,CLR 泛型將有多種語法。但是,無論采用哪種語法,用一種面向 CLR 的語言編寫的泛型代碼將可以由其他面向 CLR 的語言編寫的程序使用。我將在本文中介紹 C# 語法,其原因是,在編寫本文時,在三種較大的托管語言中,泛型的 C# 語法相當穩定。 但是,沒有必要在 Visual Basic?.NET 和 Managed C++ 的 Whidbey 版本中支持泛型。
下表顯示了泛型定義代碼和泛型引用代碼的基本 C# 語法。二者的語法區別反映了泛型代碼所涉及的雙方的不同職責。
Defining Code | Referencing Code |
---|---|
class Node<T> { T m_data; Node<T> m_next; } |
class Node8Bit : Node<Byte> { ...} |
struct Pair<T,U> { T m_element1; U m_element2; } |
Pair<Byte,String> pair; pair.m_element1 = 255; pair.m_element2 = "Hi"; |
interface IComparable<T> { Int32 CompareTo(T other); } |
class MyType : IComparable<MyType> { public Int32 CompareTo(MyType other) { ... } } |
void Swap |
Decimal d1 = 0, d2 = 2; Swap<Decimal>(ref d1, ref d2); |
delegate void EnumerateItem |
... EnumerateItem<Int32> callback = new EnumerateItem<Int32>(CallMe); } void CallMe(Int32 num) { ... } |
目前的計劃是讓 CLR(從而讓 C#)支持泛型類、結構、方法、接口和委托。 上表的左側顯示了每種定義代碼情況的 C# 語法示例。.請注意,尖括號表示類型參數列表。尖括號緊跟在泛型類型或成員的名稱后面。同樣,在類型參數列表中有一個或多個類型參數。參數還出現在泛型代碼的整個定義中,用來替代特定的 CLR 類型或作為類型構造函數的參數。 圖 4 的右側顯示了與之相匹配的引用代碼情況的 C# 語法示例。請注意,在此處,類型變量括在尖括號中;泛型標識符和括號構成一個截然不同的新標識符。另外還要注意,類型變量指定在從泛型構造類型或方法時所使用的類型。
讓我們花一點時間來定義代碼語法。當編譯器遇到一個由尖括號分開的類型參數列表時,它可識別出您在定義泛型類型或方法。泛型定義中的尖括號緊跟在所定義的類型或方法的名稱后面。
類型-參數列表指出要在泛型代碼定義中保持未指定狀態的一個或多個類型。類型參數的名稱可以是 C# 中任何有效的標識符,它們可用逗號隔開。對于“定義代碼”部分中的類型參數,需要注意下面一些事項:
在每個代碼示例中,可以看到在整個定義中(通常將出現類型名稱的位置)均使用了類型參數 T 或 U。
在 IComparable< T> 接口示例中,可以看到同時使用類型參數 T 和常規類型 Int32。在泛型代碼的定義中,既可以使用未指定的類型(通過類型參數)又可以使用指定的類型(使用 CLR 類型名稱)。
在 Node< T> 示例中,可以看到,類型參數 T 可以像在 m_data 的定義中一樣獨立使用,還可以像在 m_next 中一樣用作另一個類型構造的一部分。用作另一個泛型類型定義的變量的類型參數(如 Node< T>),稱作開放式泛型類型。用作類型參數的具體類型(如 Node< System.Byte>),稱作封閉式泛型類型。
與任何泛型方法一樣, 表格中顯示的示例泛型方法 Swap< T> 可以是泛型或非泛型類型的一部分,也可以是實例、虛擬或靜態方法。
在本文中,我對于類型參數使用的是單字符名稱(如 T 和 U),這主要是為了使情況更簡單。但是,您會發現也可以使用描述性名稱。例如,在產品代碼中,Node< T> 類型可被等效地定義為 Node< ItemType> 或 Node< dataType>。
在撰寫本文時,Microsoft 已經使庫代碼中的單字符類型參數名稱標準化,以有助于區分這些名稱與用于普通類型的名稱。我個人比較喜歡在產品代碼中使用 camelCasing 類型參數,因為這可將它們與代碼中的簡單類型名稱相區分,而同時又具有一定的描述性。
在泛型引用代碼中,未指定的類型會變成指定的類型。如果引用代碼實際使用泛型代碼,則這是十分必要的。如果您查看 圖 4 中“Referencing Code”部分中的示例,就會發現在所有情況中,新類型或方法都是通過將 CLR 類型指定為泛型的類型變量,從一個泛型構造的。在泛型語法中,諸如 Node< Byte> 和 Pair< Byte,String> 之類的代碼表示從泛型類型定義構造的新類型的類型名稱。
在深入介紹該技術本身之前,我將再介紹一個語法細節。當代碼調用泛型方法(如 Swap< T> 方法)時,完全限定的調用語法包括任何類型變量。但是,有時可以選擇將類型變量從調用語法中排除,如下面的兩行代碼所示:
- Decimal d1 = 0, d2 = 2;
- Swap(ref d1, ref d2);
這個簡化的調用語法依賴一個名為類型推理的 C# 編譯器功能,在該功能中,編譯器使用傳遞給方法的參數類型來推導類型變量。在本例中,編譯器從 d1 和 d2 的數據類型來推導,類型參數 T 的類型變量應當為 System.Decimal。如果存在多義性,類型推理對于調用方不工作,并且 C# 編譯器將會產生一個錯誤,建議您使用包含尖括號和類型變量的完整調用語法。
#p#
CLR泛型:間接
我的一個好朋友喜歡指出,大多數完美的編程解決方案都是圍繞添加另一間接層次而設計的。指針和引用允許單個函數影響一個數據結構的多個實例。虛擬函數允許單個調用站點將調用傳送到一組相似的方法 — 其中一些方法可在以后定義。這兩個間接示例非常常見,以至于程序員通常注意不到間接本身。
間接的主要目的是為了提高代碼的靈活性。泛型是一種間接形式,在這種形式中,定義不會產生可直接使用的代碼。相反,在定義泛型代碼中,會創建一個“代碼工廠”。隨后,引用代碼使用該工廠代碼來構造可直接使用的代碼。
讓我們首先從泛型方法來了解這個設計思路。 圖 5 中的代碼定義并引用了一個名為 CompareHashCodes< T> 的泛型方法。定義代碼創建了一個名為 CompareHashCodes< T> 的泛型方法,但是 圖 5 中顯示的代碼都沒有直接調用 CompareHashCodes< T>。相反,在 Main 中,引用代碼使用 CompareHashCodes< T> 來構造兩種不同的方法:CompareHashCodes< Int32> 和 CompareHashCodes< String>。這些構造方法是 CompareHashCodes< T> 的實例,它們是由引用代碼來調用的。
通常會在某個方法的定義中直接定義該方法所執行的操作。與之相反,在泛型方法的定義中,會定義它的構造方法實例將執行的操作。除了充當如何構造特定實例的模型以外,泛型方法本身不執行任何操作。CompareHashCodes< T> 是一種泛型方法,通過它可以構造對哈希代碼進行比較的方法實例。 構造實例(如 CompareHashCodes< Int32>)執行實際工作;它對整數的哈希代碼進行比較。相反,CompareHashCodes< T> 是一個從可調用中刪除的間接層。
泛型類型類似于從與其相對應的簡單副本中刪除的一個間接層。系統使用簡單的類型定義(如類或結構)來創建內存中的對象。例如,類庫中的 System.Collection.Stack 類型用于在內存中創建堆棧對象。在某種意義上,可以將 C# 中的新關鍵字或中間語言代碼中的 newobj 指令視為一個對象工廠,該對象工廠在創建對象實例時,將托管類型用作每個對象的藍圖。
另一方面,泛型類型用于實例化封閉式類型,而不是對象實例。隨后,可以使用從泛型類型構造的類型來創建對象。讓我們回顧一下在 圖 2 中定義的 Node< T> 類型以及如 圖 3所示的它的引用代碼。
托管應用程序永遠不能創建 Node< T> 類型的對象,即使它是托管類型時也是如此。這是由于 Node< T> 缺乏足夠的定義,因此無法被實例化為內存中的對象。但是,在執行應用程序的過程中,Node< T> 可用于實例化另一個類型。
Node< T> 是一個開放式泛型類型,并且只用于創建其他構造類型。如果使用 Node< T> 創建的構造類型是封閉式類型(如 Node< Int32>),則它可用于創建對象。 圖 3 中的引用代碼使用 Node< Int32> 的方式與使用簡單類型時大體相同。它創建 Node< Int32> 類型的對象,在這些對象上調用方法,等等。
泛型類型額外提供一個間接層,此功能非常強大。采用泛型類型的引用代碼會產生定制的托管類型。在腦海中將泛型代碼想象為從其簡單副本中刪除的一個間接層,這有助于憑直覺獲知 CLR 中泛型的許多行為、規則和用法。
CLR泛型小結
本文介紹了泛型類型的好處 — 如何使用它們改善類型安全、代碼重用和性能。本文還講述了 C# 中的語法以及泛型如何導致另一層間接,從而提高靈活性。
【編輯推薦】