CLR 中的范型詳解發(fā)布日期: 1/24/2005 | 更新日期: 1/24/2005
本頁內(nèi)容
在本專欄 2003 年 9 月一期中,,我初步討論了公共語言運行庫 (CLR) 中的范型。我引入了范型的概念,,介紹了范型所帶來的靈活性和代碼重用,,探討了性能和類型安全,并通過一個簡單的代碼示例展示了 C# 中的范型語法,。本月,,我將深入討論與范型有關的 CLR 內(nèi)部工作機制。我將介紹類型約束,、范型類,、方法、結(jié)構(gòu)和即將問世的范型類庫,。 編譯器如何處理范型,?C++ 模板和 Java 語言中提議的范型等效都是它們各自編譯器的功能。這些編譯器在編譯時根據(jù)對范型或模板類型的引用來構(gòu)造代碼,。這會導致代碼臃腫并降低結(jié)構(gòu)之間的類型等效(即使類型變量相同),。相反,CLR 范型不采用這種工作方式,。 CLR 中的范型是平臺本身出類拔萃的功能,。要通過這種方式實現(xiàn)它就需要更改整個 CLR(包括新的和修改過的中間語言指令),并更改元數(shù)據(jù),、類型加載器,、實時 (JIT) 編譯器、語言編譯器等等,。這對 CLR 中的運行時擴展有兩個重要的好處。 首先,,即使范型的每個結(jié)構(gòu)(例如 Node首先,,即使范型的每個結(jié)構(gòu)(例如 Node<Form> 和 Node<String>)都有自己獨特的類型標識,但 CLR 能夠在類型實例化期間重用許多真正的 JIT 編譯的代碼,。這極大地降低了代碼膨脹,,并且也是切實可行的,,因為范型的各種實例化都是在運行時才展開的。在編譯時,,構(gòu)造類型的所有內(nèi)容就是類型引用,。當程序集 A 和 B 都引用在第三方程序集中定義的范型時,它們的構(gòu)造類型就會在運行時展開,。這意味著,,除了共享 CLR 類型標識(在適當?shù)臅r候)以外,來自程序集 A 和 B 的類型實例化也共享運行時資源,,如本機代碼和擴展的元數(shù)據(jù),。 類型等效是構(gòu)造類型運行時擴展的第二個好處。以下為一個示例:引用 AssemblyA.dll 中構(gòu)造 Node <Int32> 的代碼和引用 AssemblyB.dll 中構(gòu)造 Node <Int32> 的代碼都會在運行時創(chuàng)建具有相同 CLR 類型的對象,。通過這種方式,,如果兩個程序集由同一個應用程序使用,則它們的 Node <T> 類型的結(jié)構(gòu)會解析為相同的類型,,并且它們的對象可以自由交換,。應該注意的是,編譯時擴展會使得這種邏輯上很簡單的等效變得有問題或者無法實現(xiàn),。 在運行庫級別(而非編譯器級別)實現(xiàn)范型還有其他一些好處,。其中一個好處是范型信息會在編譯和執(zhí)行期間保留下來,因此在代碼生存期的任何時刻都可以訪問它,。例如,,反射提供對范型元數(shù)據(jù)的完全訪問。另一個好處是 Visual Studio .NET 中豐富的 IntelliSense 支持,,以及范型代碼所帶來的舒心的調(diào)試體驗,。相反,Java 范型和 C++ 模板在運行時會失去它們的范型標識,。 另一個好處(也是 CLR 的支柱)是交叉語言使用 — 使用一種托管語言定義的范型可以由用另一種托管語言編寫的代碼引用。同時,,由于許多繁重的工作都是在這個平臺上完成的,,所以語言供應商在他們的編譯器中置入范型支持的可能性與日劇增。 在運行時類型擴展的眾多好處中,,我最喜歡的那一個就顯得微不足道了,。范型代碼只限明確用于類型構(gòu)造實例化的操作使用。這種限制的附加好處是,,使 CLR 范型比與它們相應的 C++ 模板更好理解,,也更加有用。讓我們看一下 CLR 中對范型的限制,。 規(guī)則和限制一個困擾使用 C++ 模板的編程人員的問題是許多對類型結(jié)構(gòu)所作的特殊嘗試都會失敗,,包括類型參數(shù)的類型變量在實現(xiàn)由模板化代碼調(diào)用的方法時會失敗,。同時,這些情況下的編譯器錯誤也很令人困擾,,而且可能看起來與根本問題不相關,。采用構(gòu)造類型的運行時擴展以后,類似的錯誤會變成 JIT 編譯器錯誤或類型加載錯誤,,而不是編譯時錯誤,。CLR 架構(gòu)師決定了對于范型來說,這種實現(xiàn)是不可接受的,。 相反,,他們決定了對于范型(例如 Node <T>)的任何可能的類型實例化,即使類型實例化實際發(fā)生在運行時,,該范型也必須在編譯時被證實為一種有效類型,。同樣,有問題的類型結(jié)構(gòu)周圍的擴展錯誤不可能出現(xiàn),。為了實現(xiàn)這個目標,,架構(gòu)師通過一組規(guī)則和限制來約束范型的功能,從而保證在嘗試擴展其中一個范型實例化之前這種范型有效,。 有一些規(guī)則限制了您通??梢跃帉懙拇a的類型。這些規(guī)則的本質(zhì)可以歸納為一句話:范型代碼只有在用于范型的每個可能的構(gòu)造實例時才有效,。否則,,范型代碼無效,并且也不能正確編譯(或者可以編譯,,但無法在運行時通過驗證),。 首先,它看起來像是一個限制規(guī)則,。以下為一個示例: public class GenericMath { public T Min<T>(T item1, T item2) { if (item1 < item2) { return item1; } return item2; } } 這段代碼在 CLR 范型中是無效的,。C# 編譯器產(chǎn)生的錯誤如下所示: invalid.cs(4,11): error CS0019: Operator ‘<‘ cannot be applied to operands of type ‘T‘ and ‘T‘ 同時,除了細微的語法區(qū)別外,,與這基本相同的代碼在 C++ 模板中是允許的,。為什么對范型有這樣的限制呢?原因是:在 C# 中,,“<”運算符只能用于特定的類型,。然而,前面代碼片段中的類型參數(shù) T 可以在運行時擴展為任何 CLR 類型,。前面的代碼示例不是在運行時被認為是無效的,,而是在編譯時被認為是無效的。 除了運算符,,更多可管理的類型使用(例如方法調(diào)用)也應用了相同的限制,。以下對 Min <T> 方法的修改也是無效的范型代碼: class GenericMath { public T Min<T>(T item1, T item2) { if (item1.CompareTo(item2) < 0) { return item1; } return item2; } } 這段代碼無效的原因與前面的示例是一樣的。雖然類庫中的許多類型都實現(xiàn)了 CompareTo 方法,,而且該方法也很容易由您的自定義類型實現(xiàn),,但不能保證這個方法適用于可用作 T 的參數(shù)的任何可能的類型。 但您也可以看出,,范型代碼中并非完全禁止方法調(diào)用,。在圖 1 中,GetHashCode 方法在兩個參數(shù)化變量中調(diào)用,,而在圖 2 中,,Node<T> 類型在它的參數(shù)化 m_data 字段中調(diào)用了 ToString 方法。為什么允許 GetHashCode 和 ToString,,而不允許 CompareTo 呢,?原因在于,GetHashCode 和 ToString 都是在 System.Object 類型中定義的,,而每個可能的 CLR 類型也是從這個類型派生的,。這意味著,類型 T 的每個可能的擴展都實現(xiàn)了 ToString 和 GetHashCode 成員函數(shù),。 如果要使范型可用于集合類以外的任何類,,則范型代碼需要能夠調(diào)用由 System.Object 定義的方法以外的方法。不過要記住,,只有當用于范型的任何可能的構(gòu)造實例時,,范型代碼才有效。有一個辦法可以解決這兩個看似相互矛盾的要求,,那就是 CLR 范型中稱為約束的功能,。 您應該知道,約束是范型或方法定義的一個可選組件,。在可作為變量用于范型代碼上的一個類型參數(shù)的類型中,,一個范型可以定義任意數(shù)量的約束,而每個約束可以應用任一個限制,。通過限制可在范型結(jié)構(gòu)中使用的類型,,對引用受限類型參數(shù)的代碼的限制就可以放松一些(請參見圖 3)。 因為對類型參數(shù) T 應用了約束,,所以 Min <T> 和 Max <T> 在其條目上調(diào)用 CompareTo 是有效的,。在圖 3 的第三行代碼中,您可以看到它引入了如下所示的 where 子句: where T : IComparable這個約束表明 Min <T> 方法的任何結(jié)構(gòu)都必須為實現(xiàn) IComparable 接口的類型的參數(shù) T 提供一個類型變量,。這個約束限制了 Min <T> 的可能實例化的種類,,但提高了方法中代碼的靈活性,比如現(xiàn)在可以在類型 T 的變量上調(diào)用 CompareTo,。 約束通過允許范型代碼調(diào)用擴展類型上的任意方法,,從而使范型算法成為可能,。雖然約束要求使用額外的語法才能定義范型代碼,但約束不會改變引用代碼的語法,。引用代碼的唯一區(qū)別在于,,類型變量必須遵守對范型的約束。例如,,以下用于 Max <T> 的引用代碼是有效的: GenericMath.Min(5, 10); 這是因為 5 和 10 都是整數(shù),,而且 Int32 類型實現(xiàn)了 IComparable 接口。然而,,試圖實現(xiàn)以下 Max <T> 結(jié)構(gòu)會產(chǎn)生編譯器錯誤: GenericMath.Min(new Object(), new Object()); 下面是編譯器生成的錯誤: MinMax.cs(32,7): error CS0309: The type ‘object‘ must be convertible to ‘System.IComparable‘ in order to use it as parameter ‘T‘ in the generic type or method ‘GenericMath.Min(T, T)‘ 對于 T,,System.Object 是一個無效的類型變量,因為它沒有實現(xiàn)對 T 的約束要求實現(xiàn)的 IComparable 接口,。當類型變量與范型代碼上的類型參數(shù)不兼容時,,約束就可能會使編譯器產(chǎn)生如前面示例中所示的描述性錯誤。 目前,,范型支持三種類型的約束:接口約束,、基類約束和構(gòu)造函數(shù)約束。接口約束指定一個接口,,該參數(shù)的所有類型變量都必須與這個接口兼容,。任意數(shù)量的接口約束都可以應用于給定的類型參數(shù)。 基類約束與接口約束類似,,但每個類型參數(shù)只能包含一個基類約束,。如果沒有為類型參數(shù)指定約束,則應用 Object 的隱式基類約束,。 構(gòu)造函數(shù)約束通過約束實現(xiàn)公共默認構(gòu)造函數(shù)的類型的類型變量,,使得范型代碼能夠創(chuàng)建由類型參數(shù)指定的類型的實例。目前,,構(gòu)造函數(shù)約束只支持默認或無參數(shù)構(gòu)造函數(shù),。 where 子句用于為給定的類型參數(shù)定義約束或約束列表。每個 where 子句只應用于一個類型參數(shù),。范型或方法定義可以沒有 where 子句,,也可以有與類型參數(shù)一樣多的 where 子句。一個給定的 where 子句可以包含一個約束,,也可以包含由逗號分隔的約束列表,。圖 4 顯示了對范型代碼應用約束的各種語法。 范型的基本規(guī)則(范型所有可能的實例化都是有效的,,或者范型本身是無效的)還有其他一些有趣的副作用,。第一個是強制類型轉(zhuǎn)換。在范型代碼中,類型參數(shù)類型的變量可能只能與它的基類約束類型或基類約束類型的基類進行相互的強制轉(zhuǎn)換,。這意味著,,如果類型參數(shù) T 沒有約束,它就只能與 Object 引用進行相互強制轉(zhuǎn)換,。然而,,如果將 T 約束為進一步派生的類型(例如 FileStream),,則范型定義可以包括 T 與 FileStream 以及與 FileStream 的所有基類(直到 Object)之間的相互強制轉(zhuǎn)換,。圖 5 中的代碼顯示了作用中的這種強制轉(zhuǎn)換規(guī)則。 在這個示例中,,T 沒有約束,,并且被視為未綁定。它具有 Object 的隱式基類約束,。T 與 Object 類之間的相互強制轉(zhuǎn)換以及與接口類型之間的相互強制轉(zhuǎn)換在編譯時是有效的,。但編譯器不允許未綁定的 T 強制轉(zhuǎn)換為其他類型(如 Int32)。范型的強制轉(zhuǎn)換規(guī)則也適用于轉(zhuǎn)換,,因此不允許使用強制轉(zhuǎn)換語法來進行類型轉(zhuǎn)換(比如從 Int32 轉(zhuǎn)換成 Int64),;范型不支持類似的轉(zhuǎn)換。 下面是另一個有趣的事情,。請考慮以下代碼: void Foo<T>() { T x = null; // compiler error when T is unbounded ••• 雖然像這樣賦空值很常使用,,但可能存在一個問題。如果將 T 擴展為值類型,,會出現(xiàn)什么情況呢,?對一個值變量賦空值沒有意義。幸運的是,,C# 編譯器提供了特殊的語法,,以保證正確的結(jié)果,而不用管 T 的運行時類型: void Foo<T>() { T x = T.default; // OK for any T} 等號右邊的表達式稱為默認值表達式,。如果將 T 擴展為引用類型,,則 T.default 會解析為 null。如果將 T 擴展為值類型,,則對于該變量,,T.default 是所有位都為零的值。 如果 T 未綁定,,則不允許對參數(shù)化變量 T 賦空值,,因此可能有人會認為以下語句也是無效的,但事實不是如此: void Foo<T>(T x) { if (x == null) { // Ok ••• } } 如果此處允許為 null,,則將 T 擴展為值類型時會出現(xiàn)什么情況呢,?如果 T 是值類型,則前面示例中的 Boolean 表達式會強制為 false。與賦空值的情況不同,,對于 T 的任何擴展,,空值比較是有意義的。 對于任何可能的實例化,,在編譯時確認范型有效這個前提的確對范型代碼有影響,。然而,我發(fā)現(xiàn)與 C++ 中的模板相比,,CLR 中范型周圍的附加結(jié)構(gòu)有明顯的作用,。總之,,約束及其周圍的基礎結(jié)構(gòu)是 CLR 范型中我最喜歡的一個方面,。 范型接口和委托范型類、結(jié)構(gòu)和方法是 CLR 范型的主要功能,。范型接口和委托是真正起支持作用的功能,。范型接口如果單獨使用,則用處有限,。但是,,當與范型類、結(jié)構(gòu)或方法一起使用時,,范型接口(和委托)就會有重要的作用,。 圖 3 中的 GenericMath.Min <T> 和 GenericMath.Max <T> 方法都將 T 約束為與 IComparable 接口兼容。這使得這些方法可以調(diào)用方法的參數(shù)化變量上的 CompareTo,。然而,,在圖 3中實現(xiàn)的這些方法都沒有充分利用范型的優(yōu)勢。原因在于,,如果接口采用一個或多個對象參數(shù)(例如 CompareTo 的 obj 參數(shù)),,則調(diào)用值類型的非范型接口會導致裝箱。 對于圖 3 中的 GenericMath.Min <T>,,如果該方法的實例化將 T 擴展為值而不是引用,,則每次調(diào)用這個方法都會導致 CompareTo 方法的參數(shù)裝箱。在這種時候,,范型接口就可以派上用場了,。 圖 6 中的代碼重構(gòu)了 GenericMath 方法,以通過范型接口 IComparable <T> 來約束 T?,F(xiàn)在,,如果 Min <T> 或 Max <T> 的實例化使用值類型作為 T 的變量,則對 CompareTo 的接口調(diào)用就是接口結(jié)構(gòu)的一部分,,它的參數(shù)是值類型,,而且沒有發(fā)生裝箱,。 范型委托具有類似于范型接口的好處,但它是面向方法而非面向類型,。 類庫中的范型除了在 CLR 中實現(xiàn)范型外,,Microsoft 還計劃提供新的范型類庫,以作為代號為“Whidbey”的 CLR 版本的類庫的一部分,。在本專欄出版的時候,,應該會推出 Whidbey CLR 的預覽版。(有關詳細信息,,請參閱本專欄的 2003 年 9 月號,。)最保守的估計,會提供實現(xiàn)列表,、詞典,、棧和隊列的范型集合類。另外,,類庫還將包含支持的接口類型(例如 IList<T>、ICollection<T> 和 IComparable<T>),,它們是 Microsoft .NET Framework 1.0 和 1.1 類庫所附帶的簡單接口的等效范型,。 最后,您還會發(fā)現(xiàn),,整個類庫中的類型都將以新的和現(xiàn)有功能的范型版本參數(shù)化,。例如,System.Array 類將包含它的 BinarySearch 和 Sort 方法的范型版本,,它們利用范型接口 IComparer<T>, 和 IComparable<T>,。在撰寫這篇文章時,Microsoft® 尚未確定要對現(xiàn)有類庫進行多大的改動,,以便在下一個運行庫版本中包含范型支持,。 小結(jié)CLR 范型是一個強大的應用程序和庫開發(fā)功能。不管您是選擇通過使用集合類來應用范型,,還是選擇通過架構(gòu)整個應用程序來應用范型,,范型都能夠使您的代碼類型更安全、可維護,,而且效率高,。在本專欄的這兩期中,在托管代碼中進行范型編程給我?guī)砹撕艽蟮臉啡?,我也期待著要發(fā)布的產(chǎn)品,。看到 CLR 增添了諸如范型這樣的重要功能是一件令人興奮的事情,。還可能有許多令人感興趣的內(nèi)容,。請繼續(xù)關注我們的工作,。
Jason Clark 為 Microsoft 和 Wintellect (http://www.) 提供培訓和咨詢,,他曾經(jīng)是 Windows NT 和 Windows 2000 Serv% |
|