簡單介紹CLR泛型及其優(yōu)勢
泛型是對 CLR 類型系統(tǒng)的擴(kuò)展,它允許開發(fā)人員定義那些未指定某些細(xì)節(jié)的類型。相反,當(dāng)用戶代碼引用該代碼時(shí),就會指定這些細(xì)節(jié)。引用泛型的代碼填充缺少的細(xì)節(jié),并根據(jù)其特定需求對類型進(jìn)行調(diào)整。泛型的命名反映了該功能的目標(biāo):允許在編寫代碼時(shí)不指定可能限制其使用范圍的細(xì)節(jié)。代碼本身就是泛型。稍后,我會對它進(jìn)行更詳細(xì)的介紹。
CLR泛型預(yù)覽
正如使用任何新技術(shù)一樣,明白它的好處所在會有所幫助。那些熟悉 C++ 模板的用戶將會發(fā)現(xiàn),泛型在托管代碼中具有相似的用途。但是,我不愿意對 CLR 泛型和 C++ 模板進(jìn)行過多比較,因?yàn)榉盒途哂幸恍╊~外的好處,它不存在以下兩個(gè)常見問題:代碼臃腫和開發(fā)人員混淆。
CLR 泛型具有一些好處,如編譯時(shí)類型安全、二進(jìn)制代碼重用、性能和清晰性。我將簡要介紹這些好處,您在閱讀本專欄的其余文章時(shí),會更詳細(xì)地了解它們。例如,假設(shè)有兩個(gè)集合類:SortedList(Object 引用的集合)和 GenericSortedList< T>(任意類型的集合)。
類型安全:當(dāng)用戶向 SortedList 類型的集合內(nèi)添加 String 時(shí),String 會隱式強(qiáng)制轉(zhuǎn)換為 Object。同樣,如果從該列表中檢索 String 對象,則它必須在運(yùn)行時(shí)從 Object 引用強(qiáng)制轉(zhuǎn)換到 String 引用。這會造成編譯時(shí)缺少類型安全,從而使開發(fā)人員感到厭煩,并且易于出錯(cuò)。相反,如果使用 GenericSortedList< String>(T 的類型被設(shè)置為 String),就會使所有的添加和查找方法使用 String 引用。這允許在編譯時(shí)(而非運(yùn)行時(shí))指定和檢查元素的類型。
二進(jìn)制代碼重用:為了進(jìn)行維護(hù),開發(fā)人員可以選擇使用 SortedList ,通過從它派生 SortedListOfString 來實(shí)現(xiàn)編譯時(shí)的類型安全。此方法有一個(gè)問題,那就是必須對于每個(gè)需要類型安全列表的類型都編寫新代碼,而這會很快變成非常費(fèi)力的工作。使用 GenericSortedList< T>,需要執(zhí)行的全部操作就是將具有所需元素類型的類型實(shí)例化為 T。泛型代碼還有一個(gè)附加價(jià)值,那就是它在運(yùn)行時(shí)生成,因此,對于無關(guān)元素類型的兩個(gè)擴(kuò)展(如 GenericSortedList< String> 和 GenericSortedList< FileStream>)能夠重新使用同一個(gè)實(shí)時(shí) (JIT) 編譯代碼的大部分。CLR 只是處理細(xì)節(jié) — 代碼不再臃腫!
性能:關(guān)鍵在于:如果類型檢查在編譯時(shí)間進(jìn)行,而不是在運(yùn)行時(shí)間進(jìn)行,則性能增強(qiáng)。在托管代碼中,引用和值之間的強(qiáng)制轉(zhuǎn)換既會導(dǎo)致裝箱又會導(dǎo)致取消裝箱,而且避免這樣的強(qiáng)制轉(zhuǎn)換可能會對性能產(chǎn)生同樣的負(fù)面影響。最近針對一個(gè)由一百萬個(gè)整數(shù)組成的數(shù)組進(jìn)行了快速排序法基準(zhǔn)測試,結(jié)果表明泛型方法比非泛型方法快三倍。這是由于完全避免了對這些值進(jìn)行裝箱。如果針對由字符串引用組成的數(shù)組進(jìn)行同樣的排序,則由于無需在運(yùn)行時(shí)執(zhí)行類型檢查,因此使用泛型方法后性能提高了 20%。
清晰性:泛型的清晰性體現(xiàn)在許多方面。約束是泛型的一個(gè)功能,它們會禁止對泛型代碼進(jìn)行不兼容的擴(kuò)展;使用泛型,您將不再面臨那些困擾 C++ 模板用戶的含混不清的編譯器錯(cuò)誤。在 GenericSortedList< T> 示例中,集合類將有一個(gè)約束,該約束使集合類只處理可進(jìn)行比較并依此進(jìn)行排序的 T 類型。同樣,通??梢允褂妹麨轭愋屯评淼墓δ軄碚{(diào)用泛型方法,而無需使用任何特殊語法。當(dāng)然,編譯時(shí)類型安全可以使應(yīng)用程序代碼更加清晰。 我將在本文中詳細(xì)介紹約束、類型推理和類型安全。
#p#
一個(gè)簡單的CLR泛型示例
Whidbey CLR 版本將通過類庫中的一套泛型集合類來提供這些現(xiàn)成的好處。但是,可通過為應(yīng)用程序定義其自己的泛型代碼,使其進(jìn)一步受益于泛型。為了解釋這是如何完成的,我將首先修改一個(gè)簡單的鏈接列表節(jié)點(diǎn)類,使其成為泛型類類型。
以下代碼中的 Node 類只是包括一些基本內(nèi)容。它有兩個(gè)字段:m_data(引用節(jié)點(diǎn)的數(shù)據(jù))和 m_data(引用鏈接列表中的下一項(xiàng))。這兩個(gè)字段都是由構(gòu)造函數(shù)方法設(shè)置的。確實(shí)只有兩個(gè)其他點(diǎn)綴性功能,第一個(gè)功能是通過名為 Data 和 Next 的只讀屬性訪問 m_data 和 m_next 字段。第二個(gè)功能是對 System.Object 的 ToString 虛擬方法進(jìn)行重寫。
- 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 類的代碼。該引用代碼會受到某些限制。問題在于,為了能在許多上下文中使用,其數(shù)據(jù)必須為最基本的類型,即 System.Object。這意味著使用 Node 時(shí),就會失去任何形式的編譯時(shí)類型安全。使用 Object 意味著算法或數(shù)據(jù)結(jié)構(gòu)中的“任意類型”會強(qiáng)迫所使用的代碼在 Object 引用和實(shí)際數(shù)據(jù)類型之間進(jìn)行強(qiáng)制轉(zhuǎn)換。應(yīng)用程序中的任何類型不匹配錯(cuò)誤只有在運(yùn)行之后才被捕獲。如果在運(yùn)行時(shí)嘗試進(jìn)行強(qiáng)制轉(zhuǎn)換,這些錯(cuò)誤會采用 InvalidCastException 形式。
此外,如果要向 Object 引用賦予任何基元值(如 Int32),則需要對實(shí)例進(jìn)行裝箱。裝箱涉及到內(nèi)存分配和內(nèi)存復(fù)制,以及最后對已裝箱值進(jìn)行的垃圾回收。最后,正如可在 圖 1 中看到的那樣,從 Object 引用強(qiáng)制轉(zhuǎn)換為值類型(如 Int32)會導(dǎo)致取消裝箱(也包括類型檢查)。 由于裝箱和取消裝箱會損害該算法的整體性能,因此您會明白為什么用 Object 就意味著“任何類型”都具有一定的缺點(diǎn)。
使用泛型重寫 Node 是解決這些問題的完美方法。讓我們看一下下面的代碼,您將發(fā)現(xiàn) Node 類型被重寫為 Node< T> 類型。具有泛型行為的類型(如 Node< T>)是參數(shù)化類型,并且可被稱作 Parameterized Node、Node of T 或泛型Node。稍后我將介紹這個(gè)新的 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 類型在功能和結(jié)構(gòu)上相似。二者均支持為任何給定類型的數(shù)據(jù)構(gòu)建鏈接列表。但是,Node 使用 System.Object 來表示“任意類型”,而 Node< T> 不指定該類型。相反,Node< T> 使用名為 T 且作為類型占位符的類型參數(shù)。當(dāng)使用者代碼使用 Node< T> 時(shí),名為 T 的類型參數(shù)最終由 Node< T> 的參數(shù)來指定。
- 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 位帶符號整數(shù)的 Node< T>,這是通過構(gòu)造類似類型名稱:Node< Int32> 來實(shí)現(xiàn)的。在本例中,Int32 是類型參數(shù) T 的類型變量。(順便說一句,C# 還將接受Node< int>,以便將 T 指示為 Int32。) 如果該代碼需要某種其他類型(如 String 引用)的鏈接列表,則這可通過將它指定為 T 的類型變量來完成,例如:Node< String>。
Node< T> 的好處在于:它的算法行為可被明確定義,而它所操作的數(shù)據(jù)類型仍保持未指定狀態(tài)。因此,Node< T> 類型在工作方式上是具體的;而泛型在所處理的內(nèi)容方面又是具體的??傊?,諸如鏈接列表應(yīng)當(dāng)擁有的數(shù)據(jù)類型等細(xì)節(jié)最好留給使用 Node< T> 的代碼來指定。
在討論泛型時(shí),最好先明確兩種角色:定義代碼和引用代碼。定義代碼包括既聲明泛型代碼存在又定義類型成員(如方法和字段)的代碼。 圖 2 中顯示的是類型 Node< T> 的定義代碼。引用代碼是用戶代碼,它使用預(yù)定義的泛型代碼,并且該代碼還可以內(nèi)置到另一個(gè)程序集中。 圖 3 是 Node< T> 的引用代碼示例。
考慮定義代碼和引用代碼非常有用,原因在于這兩種角色都在實(shí)際的可使用泛型代碼構(gòu)造中起著一定的作用。 圖 3 中的引用代碼使用 Node< T> 來構(gòu)造一個(gè)名為 Node< T> 的新類型。Node< Int32> 是一個(gè)截然不同的類型,它由以下兩個(gè)關(guān)鍵成分構(gòu)建而成:Node< T>(由定義代碼創(chuàng)建),參數(shù) T 的類型變量 Int32(由引用代碼指定)。只有使用這兩個(gè)成分才能使泛型代碼變得完整。
請注意,從面向?qū)ο蟮呐缮嵌瓤?,泛型類型(?Node< T>)以及從泛型類型構(gòu)造的類型(如 Node< Int32> 或 Node< String>)并不是相關(guān)類型。類型 Node< Int32>、Node< String> 和 Node< T> 類型是同輩,它們都是從 System.Object 直接派生而來。
#p#
C# Generic 語法
CLR 支持多種編程語言,因此,CLR 泛型將有多種語法。但是,無論采用哪種語法,用一種面向 CLR 的語言編寫的泛型代碼將可以由其他面向 CLR 的語言編寫的程序使用。我將在本文中介紹 C# 語法,其原因是,在編寫本文時(shí),在三種較大的托管語言中,泛型的 C# 語法相當(dāng)穩(wěn)定。 但是,沒有必要在 Visual Basic?.NET 和 Managed C++ 的 Whidbey 版本中支持泛型。
下表顯示了泛型定義代碼和泛型引用代碼的基本 C# 語法。二者的語法區(qū)別反映了泛型代碼所涉及的雙方的不同職責(zé)。
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) { ... } |
目前的計(jì)劃是讓 CLR(從而讓 C#)支持泛型類、結(jié)構(gòu)、方法、接口和委托。 上表的左側(cè)顯示了每種定義代碼情況的 C# 語法示例。.請注意,尖括號表示類型參數(shù)列表。尖括號緊跟在泛型類型或成員的名稱后面。同樣,在類型參數(shù)列表中有一個(gè)或多個(gè)類型參數(shù)。參數(shù)還出現(xiàn)在泛型代碼的整個(gè)定義中,用來替代特定的 CLR 類型或作為類型構(gòu)造函數(shù)的參數(shù)。 圖 4 的右側(cè)顯示了與之相匹配的引用代碼情況的 C# 語法示例。請注意,在此處,類型變量括在尖括號中;泛型標(biāo)識符和括號構(gòu)成一個(gè)截然不同的新標(biāo)識符。另外還要注意,類型變量指定在從泛型構(gòu)造類型或方法時(shí)所使用的類型。
讓我們花一點(diǎn)時(shí)間來定義代碼語法。當(dāng)編譯器遇到一個(gè)由尖括號分開的類型參數(shù)列表時(shí),它可識別出您在定義泛型類型或方法。泛型定義中的尖括號緊跟在所定義的類型或方法的名稱后面。
類型-參數(shù)列表指出要在泛型代碼定義中保持未指定狀態(tài)的一個(gè)或多個(gè)類型。類型參數(shù)的名稱可以是 C# 中任何有效的標(biāo)識符,它們可用逗號隔開。對于“定義代碼”部分中的類型參數(shù),需要注意下面一些事項(xiàng):
在每個(gè)代碼示例中,可以看到在整個(gè)定義中(通常將出現(xiàn)類型名稱的位置)均使用了類型參數(shù) T 或 U。
在 IComparable< T> 接口示例中,可以看到同時(shí)使用類型參數(shù) T 和常規(guī)類型 Int32。在泛型代碼的定義中,既可以使用未指定的類型(通過類型參數(shù))又可以使用指定的類型(使用 CLR 類型名稱)。
在 Node< T> 示例中,可以看到,類型參數(shù) T 可以像在 m_data 的定義中一樣獨(dú)立使用,還可以像在 m_next 中一樣用作另一個(gè)類型構(gòu)造的一部分。用作另一個(gè)泛型類型定義的變量的類型參數(shù)(如 Node< T>),稱作開放式泛型類型。用作類型參數(shù)的具體類型(如 Node< System.Byte>),稱作封閉式泛型類型。
與任何泛型方法一樣, 表格中顯示的示例泛型方法 Swap< T> 可以是泛型或非泛型類型的一部分,也可以是實(shí)例、虛擬或靜態(tài)方法。
在本文中,我對于類型參數(shù)使用的是單字符名稱(如 T 和 U),這主要是為了使情況更簡單。但是,您會發(fā)現(xiàn)也可以使用描述性名稱。例如,在產(chǎn)品代碼中,Node< T> 類型可被等效地定義為 Node< ItemType> 或 Node< dataType>。
在撰寫本文時(shí),Microsoft 已經(jīng)使庫代碼中的單字符類型參數(shù)名稱標(biāo)準(zhǔn)化,以有助于區(qū)分這些名稱與用于普通類型的名稱。我個(gè)人比較喜歡在產(chǎn)品代碼中使用 camelCasing 類型參數(shù),因?yàn)檫@可將它們與代碼中的簡單類型名稱相區(qū)分,而同時(shí)又具有一定的描述性。
在泛型引用代碼中,未指定的類型會變成指定的類型。如果引用代碼實(shí)際使用泛型代碼,則這是十分必要的。如果您查看 圖 4 中“Referencing Code”部分中的示例,就會發(fā)現(xiàn)在所有情況中,新類型或方法都是通過將 CLR 類型指定為泛型的類型變量,從一個(gè)泛型構(gòu)造的。在泛型語法中,諸如 Node< Byte> 和 Pair< Byte,String> 之類的代碼表示從泛型類型定義構(gòu)造的新類型的類型名稱。
在深入介紹該技術(shù)本身之前,我將再介紹一個(gè)語法細(xì)節(jié)。當(dāng)代碼調(diào)用泛型方法(如 Swap< T> 方法)時(shí),完全限定的調(diào)用語法包括任何類型變量。但是,有時(shí)可以選擇將類型變量從調(diào)用語法中排除,如下面的兩行代碼所示:
- Decimal d1 = 0, d2 = 2;
- Swap(ref d1, ref d2);
這個(gè)簡化的調(diào)用語法依賴一個(gè)名為類型推理的 C# 編譯器功能,在該功能中,編譯器使用傳遞給方法的參數(shù)類型來推導(dǎo)類型變量。在本例中,編譯器從 d1 和 d2 的數(shù)據(jù)類型來推導(dǎo),類型參數(shù) T 的類型變量應(yīng)當(dāng)為 System.Decimal。如果存在多義性,類型推理對于調(diào)用方不工作,并且 C# 編譯器將會產(chǎn)生一個(gè)錯(cuò)誤,建議您使用包含尖括號和類型變量的完整調(diào)用語法。
#p#
CLR泛型:間接
我的一個(gè)好朋友喜歡指出,大多數(shù)完美的編程解決方案都是圍繞添加另一間接層次而設(shè)計(jì)的。指針和引用允許單個(gè)函數(shù)影響一個(gè)數(shù)據(jù)結(jié)構(gòu)的多個(gè)實(shí)例。虛擬函數(shù)允許單個(gè)調(diào)用站點(diǎn)將調(diào)用傳送到一組相似的方法 — 其中一些方法可在以后定義。這兩個(gè)間接示例非常常見,以至于程序員通常注意不到間接本身。
間接的主要目的是為了提高代碼的靈活性。泛型是一種間接形式,在這種形式中,定義不會產(chǎn)生可直接使用的代碼。相反,在定義泛型代碼中,會創(chuàng)建一個(gè)“代碼工廠”。隨后,引用代碼使用該工廠代碼來構(gòu)造可直接使用的代碼。
讓我們首先從泛型方法來了解這個(gè)設(shè)計(jì)思路。 圖 5 中的代碼定義并引用了一個(gè)名為 CompareHashCodes< T> 的泛型方法。定義代碼創(chuàng)建了一個(gè)名為 CompareHashCodes< T> 的泛型方法,但是 圖 5 中顯示的代碼都沒有直接調(diào)用 CompareHashCodes< T>。相反,在 Main 中,引用代碼使用 CompareHashCodes< T> 來構(gòu)造兩種不同的方法:CompareHashCodes< Int32> 和 CompareHashCodes< String>。這些構(gòu)造方法是 CompareHashCodes< T> 的實(shí)例,它們是由引用代碼來調(diào)用的。
通常會在某個(gè)方法的定義中直接定義該方法所執(zhí)行的操作。與之相反,在泛型方法的定義中,會定義它的構(gòu)造方法實(shí)例將執(zhí)行的操作。除了充當(dāng)如何構(gòu)造特定實(shí)例的模型以外,泛型方法本身不執(zhí)行任何操作。CompareHashCodes< T> 是一種泛型方法,通過它可以構(gòu)造對哈希代碼進(jìn)行比較的方法實(shí)例。 構(gòu)造實(shí)例(如 CompareHashCodes< Int32>)執(zhí)行實(shí)際工作;它對整數(shù)的哈希代碼進(jìn)行比較。相反,CompareHashCodes< T> 是一個(gè)從可調(diào)用中刪除的間接層。
泛型類型類似于從與其相對應(yīng)的簡單副本中刪除的一個(gè)間接層。系統(tǒng)使用簡單的類型定義(如類或結(jié)構(gòu))來創(chuàng)建內(nèi)存中的對象。例如,類庫中的 System.Collection.Stack 類型用于在內(nèi)存中創(chuàng)建堆棧對象。在某種意義上,可以將 C# 中的新關(guān)鍵字或中間語言代碼中的 newobj 指令視為一個(gè)對象工廠,該對象工廠在創(chuàng)建對象實(shí)例時(shí),將托管類型用作每個(gè)對象的藍(lán)圖。
另一方面,泛型類型用于實(shí)例化封閉式類型,而不是對象實(shí)例。隨后,可以使用從泛型類型構(gòu)造的類型來創(chuàng)建對象。讓我們回顧一下在 圖 2 中定義的 Node< T> 類型以及如 圖 3所示的它的引用代碼。
托管應(yīng)用程序永遠(yuǎn)不能創(chuàng)建 Node< T> 類型的對象,即使它是托管類型時(shí)也是如此。這是由于 Node< T> 缺乏足夠的定義,因此無法被實(shí)例化為內(nèi)存中的對象。但是,在執(zhí)行應(yīng)用程序的過程中,Node< T> 可用于實(shí)例化另一個(gè)類型。
Node< T> 是一個(gè)開放式泛型類型,并且只用于創(chuàng)建其他構(gòu)造類型。如果使用 Node< T> 創(chuàng)建的構(gòu)造類型是封閉式類型(如 Node< Int32>),則它可用于創(chuàng)建對象。 圖 3 中的引用代碼使用 Node< Int32> 的方式與使用簡單類型時(shí)大體相同。它創(chuàng)建 Node< Int32> 類型的對象,在這些對象上調(diào)用方法,等等。
泛型類型額外提供一個(gè)間接層,此功能非常強(qiáng)大。采用泛型類型的引用代碼會產(chǎn)生定制的托管類型。在腦海中將泛型代碼想象為從其簡單副本中刪除的一個(gè)間接層,這有助于憑直覺獲知 CLR 中泛型的許多行為、規(guī)則和用法。
CLR泛型小結(jié)
本文介紹了泛型類型的好處 — 如何使用它們改善類型安全、代碼重用和性能。本文還講述了 C# 中的語法以及泛型如何導(dǎo)致另一層間接,從而提高靈活性。
【編輯推薦】