1. 程式人生 > >第十二章 泛型

第十二章 泛型

目錄:

12.1 FCL中的泛型

12.2 泛型基礎結構

12.3 泛型介面

12.4 泛型委託

12.5 委託和介面的逆變和協變泛型型別實參

12.6 泛型方法

12.7 泛型和其他成員

12.8 可驗證性和約束

 

泛型時CLR和程式語言提供的一種特殊機制,它支援另一種形式的程式碼重用,即“演算法重用”。

大多數演算法允許都封裝在一個型別中,CLR允許建立泛型引用型別和泛型值型別,但不允許建立泛型列舉型別。CLR還允許建立泛型介面和泛型委託。方法偶爾也封裝有用的演算法,所以CLR允許在引用型別,值型別或介面中定義泛型方法。

定義泛型型別或方法時,為型別指定的任何變數(比如T)都稱為型別引數。

T是變數名,原始碼能使用資料型別的任何地方都能使用T。

使用泛型型別或方法時指定的具體資料型別稱為型別實參。

優勢:

原始碼保護:使用泛型演算法的開發人員不需要訪問演算法的原始碼。

型別安全:將泛型演算法應用於一個具體的型別時,編譯器和CLR能理解開發人員的意圖,並保證只有與指定資料型別相容的物件才能用於演算法。

更清晰的程式碼:由於編譯器強制型別安全性,所以減少了原始碼中必須進行的強制型別轉換次數,使程式碼易於編寫和維護。

更佳的效能:CLR不需要執行裝箱操作,值型別的例項以傳值的方式傳遞。

12.1 FCL中的泛型

泛型最明顯的應用就是集合類。集合型別實現了許多介面,放入集合中的物件可實現介面來執行排序和搜尋等操作。

12.2 泛型基礎結構

CLR新增泛型需要的工作:

建立新的IL指令,使之能夠識別型別引數。

修改現有元資料表的格式,以便表示具有泛型引數的型別名稱和方法。

修改各種程式語言(C#,Microsoft Visual Basic .NET等)來支援新語法,允許開發人員定義和引用泛型型別和方法。

修改編譯器,使之能生成新的IL指令和修改的元資料格式。

修改JIT編譯器,以便處理新的支援型別實參的IL指令來生成正確的本機程式碼。

建立新的反射成員,使開發人員能查詢型別和成員,以判斷它們是否具有泛型引數。另外,還必須定義新的反射成員,使開發人員能在執行時建立泛型型別和方法定義。

修改偵錯程式以顯示和操縱泛型型別,成員,欄位以及區域性變數。

修改VS的“智慧感知”功能。將泛型型別或方法應用於特性資料型別時能顯示成員的原型。

12.2.1 開放型別和封閉型別

具有泛型型別引數的型別成為開發型別,CLR禁止構造開放型別的任何例項。這類似於CLR禁止構造介面型別的例項。

程式碼引用泛型型別時可指定一組泛型型別實參。為所有型別引數都傳遞了實際的資料型別,型別就成為封閉型別。CLR允許構造封閉型別的例項。然而,程式碼引用泛型型別的時候,可能留下一些泛型型別實參未指定。這會在CLR中建立新的開發型別物件,而且不能建立該型別的例項。

12.2.2 泛型型別和繼承

泛型型別仍然時型別,所以能從其他任何型別派生。使用泛型型別並指定型別實參時,實際是在CLR中定義一個新的型別物件,新的型別物件從泛型型別派生自的那個型別派生。

12.2.3 泛型型別同一性

12.2.4 程式碼爆炸

使用泛型型別引數的方法在進行JIT編譯時,CLR獲取方法的IL,用指定的型別實參替換,然後建立恰當的本機程式碼(這些程式碼未操作指定資料型別“量身定製”)。缺點:CLR要為每種不同的方法/型別組合生成本機程式碼。這個現象時“程式碼爆炸”。

CLR內建了一些優化措施能緩解程式碼爆炸。假如為特定的型別實參呼叫了一個方法,以後再用相同的型別實參呼叫這個方法,CLR只會為這個方法/型別組合編譯一次程式碼。

CLR還有另一優化,它認為所有引用型別實參都完全相同,所以程式碼能夠共享。(所有引用型別的引數或變數實際上只是指向堆上物件的指標,所有物件指標都以相同方式操縱)

如果某個型別時值型別,CLR就必須專門為那個值型別生成本機程式碼。因為值型別的大小不定,CLR無法共享程式碼,因為可能要用不同的本機CPU指令來操縱這些值。

12.3 泛型介面

使用非泛型介面來操縱值型別會發生裝箱,而且會失去編譯時的型別安全性。更到內容請參考在13章介面

12.4 泛型委託

CLR支援泛型委託,目的是保證任何型別的物件都能以型別安全的方式傳給回撥函式。泛型委託允許值型別例項在傳給給回撥方法時不進行任何裝箱。 

12.5 委託和介面的逆變和協變泛型型別實參

委託的每個泛型型別引數都可標記為協變數或逆變數。這樣就可將泛型委託型別的變數轉換為相同的委託型別(但泛型引數型別不同)。泛型型別引數:

不變數:意味著泛型型別引數不能更改。

逆變數:意味著泛型型別引數可以從一個類更改為它的某個派生類。在C#中用in關鍵字標記逆變數形式的泛型型別引數。逆變數泛型型別引數只出現在輸入位置,比如作為方法的引數。

協變數:意味著泛型型別引數可以從一個類更改為它的某個基類。C#是用out關鍵字標記協變數形式的泛型型別引數。協變數泛型型別引數只能出現在輸出位置,比如作為方法的返回型別。

(協變性指定返回型別的相容性,逆變性指定引數的相容性)

對於泛型型別引數,如果要將該型別的實參傳給使用out或ref關鍵的方法,便不允許可變性。

12.6 泛型方法

定義泛型類,結構或方法時,型別中定義的任何地方都可引用型別指定的型別引數。型別引數可作為方法引數,方法返回值或方法內部定義的區域性變數的型別使用。CLR也允許方法指定它自己的型別引數。這些引數也可以作為引數,返回值,或區域性變數使用。

泛型方法和型別推斷

C#編譯器支援在呼叫泛型方法時進行型別推斷。這意味著編譯器會在呼叫泛型方法時自動推斷要使用的型別。

12.7 泛型和其他成員

在C#中,屬性,索引器,事件,操作符方法,構造器和終結器本身不能有型別引數。但它們能在泛型型別中定義,而且這些成員中的程式碼能使用型別的型別引數。

12.8 可驗證性和約束

 約束機制:約束的作用是限制能指定成泛型實參的型別數量。通過限制類型的數量,可以對那些型別執行更多操作。

約束可應用於泛型型別的型別引數,也可用於泛型方法的型別引數。CLR不允許基於型別引數名稱或約束來進行過載;只能基於元數(型別引數個數)對型別或方法進行過載。

重寫虛泛型方法時,重寫的方法必須指定相同數量的型別引數,而且這些型別引數繼承在基類方法上指定的約束。事實上,根本不允許為重寫方法的型別引數指定任何約束。但型別引數的名稱是可以改變的。類似地,實現介面方法時,方法必須指定與介面方法等量地型別引數,這些型別引數將繼承由介面方法指定地約束。

12.8.1 主要約束

型別引數可以指定零個或者一個主要約束。主要約束可以代表非密封類地一個引用型別。

指定引用型別約束時,相當於向編譯器承諾:一個指定的型別實參要麼時與約束型別相同的型別,要麼時從約束型別派生的型別。

12.8.2 次要約束

型別引數可以指定零個或者多個次要約束,次要約束代表介面型別。這種約束向編譯器承諾型別實參實現了介面。由於能指定多個介面約束,所以型別實參必須實現了所有介面約束。

還有一種次要約束稱為型別引數約束,有時也稱為裸型別約束。這種約束用得比介面約束少得多。它允許一個泛型型別或方法規定:指定的型別實參要麼就是約束的型別,要麼時約束的型別的派生類。一個型別引數可以指定零個或者多個型別引數約束。

12.8.3 構造器約束

型別引數可指定零個或一個構造器約束,它向編譯器承諾型別實參是是實現了公共無參構造器的非抽象型別。where T : new()

12.8.4 其他可驗證性問題

1.泛型型別變數的轉型

將泛型型別的變數轉型為其他型別是非法的,除非轉型為與約束相容的型別。

2.將泛型型別變數設為預設值

將泛型型別變數設為null是非法的,除非將泛型型別約束成引用型別。

3.將泛型型別變數與null進行比較

無論泛型型別是否被約束,使用==或!=操作符將泛型型別變數與null進行比較都是合法的。

4.兩個泛型型別變數相互比較

如果泛型型別引數不能肯定是引用型別,對同一個泛型型別的兩個變數進行比較是非法的。

5.泛型型別變數作為運算元使用

將操作符應用於泛型型別運算元會出現大量問題。不能將操作符應用於泛型型別的變數。