《深入理解C#》整理1-泛型

泛型是C#2最重要的新特性,同時也是.NET2.0的CLR中最重要的新特性,它實現了類型和方法的參數化(可作爲參數傳遞)。它們增強了性能,是代碼更富表現力,並且將大量安全檢查從執行時轉移到了編譯時進行。

1、爲什麼需要泛型

1、使用如ArrayList這類爲不同數據類型而設計的類型時,每次foreach都需要隱式的強制轉換。強制轉換意味着本因由我們爲編譯器提供更多的信息,轉變爲讓編譯器生成一個檢查以便執行時運行,這是一個糟糕的選擇。如果需要將某處的信息傳遞給編譯器,那麼使用你代碼的人同樣需要該類信息,而保留這類信息的理想位置通常是聲明變量或方法的位置。有了泛型,用戶在程序中使用錯誤的參數調用庫時,就無法通過編譯(注意編譯時運行檢查相比執行時檢查的重要性)。

2、由於編譯器能執行更多的檢查,所以執行時檢查就能少做;其次,JIT能夠更爲聰明的處理值類型,能消除很多情況下的裝箱拆箱處理;某些情況下,無論是速度還是內存消耗上,使用泛型都會表現的更爲優異。泛型的好處就像靜態語言較之動態語言的優點:更好的編譯時檢查,更多代碼中能夠直接表現的信息,更多的IDE支持,更好的性能。

2、日常使用的泛型

1、泛型有兩種形式,泛型類型(包括類、接口、委託、結構)和泛型方法。類型參數是真實類型的佔位符,在泛型聲明中,類型參數(type parameter)需要放在一對尖括號內,以逗號隔開;而使用泛型類型或方法時,需要用真實的類型代替,這類真實的類型即爲類型實參(type argument)。

2、如果沒有爲泛型類型提供類型實參,那麼就是一個未綁定泛型類型;如果指定了類型實參,那麼該類型就是一個已構造類型。如果類型是對象的藍圖,那麼未綁定泛型類型就是已構造類型的藍圖,它是一種額外的抽象層。而已構造類型可以是開放或關封閉的,開放類型包含一個類型參數,而封閉類型則是不開放的,類型中的每個部分都是明確的。所有代碼實際上都是在一個封閉的已構造類型的上下文中執行的。

image-20201020194027956

3、在泛型類型中,構造函數不在尖括號中列出類型參數,類型參數從屬於類型,而非從屬於某個特定的構造函數,所以纔會在聲明類型時聲明。成員(僅限方法)僅在引入新的類型參數時才需要聲明。泛型類型是可以重載的,只需要改變一下參數的數量就可以了,這一點同樣適用於泛型方法。

4、泛型方法示例(需要注意的是非泛型類型也可以實現泛型方法):

image-20201020200404429

3、深化與提高

3.1泛型約束

1、引用類型約束:用於確保使用的類型實參是引用類型,表示成T:Class,且必須是爲類型參數指定的第一個約束。類型實參可以是任何類、接口、數組、委託或者是已知引用類型的另一個類型參數。使用此類約束後,可以使用==和!=來比較引用,但是需要注意的是,除非還存在其他約束,否則只能比較引用,即使該類型中重載了那些操作符。

2、值類型約束:約束表示成T:struct,可以確保使用類型的實參是值類型,包括枚舉,但是它將可空類型排除在外。類型參數被約束爲值類型後,就不允許使用==和!=進行比較

3、構造函數類型約束:表示成T:new(),必須是所有類型參數的最後一個約束,它檢查類型實參是否有一個可以用於創建類型實參的無參構造函數。這適用於所有值類型,所有沒有顯式聲明構造函數的非靜態、非抽象類,以及所有顯式聲明瞭一個公共無參構造函數的非抽象類。

C#規範規定,所有值類型都有一個默認的無參構造函數,而且顯式聲明的構造函數和無參構造函數是用相同的語法來調用的,具體調用哪一個,需要依賴於編譯器正在底層進行的工作。CLI規範則沒有這些要求,不過它提供了一個特殊的指令,可以在不指定任何參數的情況下創建默認值。

4、轉換類型約束:允許你指定另一個類型,類型參數必須可以通過一致性、引用或裝箱轉換隱式地轉換爲該類型。你還可以規定一個類型實參必須可以轉換爲另一個類型實參,這稱爲類型參數約束。示例如下:

image-20201021194027610

轉換類型約束無法指定枚舉約束和委託約束,但其並非是CLR的限制,如果在IL中構造適當的代碼是可以工作的

5、組合約束:沒有類型即是引用類型又是值類型,所以不允許此類組合;每個值類型都有一個無參構造函數,所以假如已經有一個值類型約束,就不允許再指定一個構造函數約束;如果存在多個轉換類型約束,並且其中一個爲類,那麼它應該出現在接口的前面,並且不能多次指定同一個接口。

規範中對約束的分類略有不同,它將其劃分爲主要約束、次要約束和構造函數約束。主要約束可以分爲引用類型約束、值類型約束或使用類的轉換類型約束;次要約束爲使用接口或其他類型參數的轉換類型約束。主要約束是可選的,但只能有一個;次要約束則可以有多個;構造函數約束也是可選的。

3.2、泛型方法類型實參的類型推斷

爲了簡化工作,C#2編譯器被賦予了一定的智能,讓我們在調用方法時,可以不需要顯式聲明類型實參。其基本步驟如下:

①對於每一個方法實參,都嘗試用十分簡單的技術推斷出泛型方法的一些類型實參;

②驗證步驟1中的所有結果都是一致的,如果不同方法推斷出的類型實參爲不同類型,推斷會失敗;

③驗證泛型方法需要的所有類型實參都已經被推斷出來,但也存在不能讓編譯器推斷的部分,可以顯式的指定,但必須全部顯式指定

此外需要注意的時類型推斷只適用於泛型方法,不適用於泛型類型。

3.3、實現泛型

實現泛型時需要留意幾個方面:

1、默認表達式:泛型默認值很少使用,但也有像例如Dictionary<Tkey,TValue>這樣可以使用TValue填充輸出參數的情況(在方法正常返回前需要賦值);所以C#2提供了默認值表達式default(T);

2、直接比較:

①如果一個類型參數是未約束的,那麼且只能在將該類型的值與null進行比較時才能使用==和!=操作符

②不能直接比較兩個T類型的值

③如果類型實參是一個引用類型,會進行正常的引用比較

④如果爲T提供的類型實參是一個非可空值類型,與null進行比較的結果總是顯示它們不相等

⑤如果類型實參是可空值類型,那麼就會自然而然地與類型的空值進行比較

⑥如果一個類型參數被約束成值類型,就完全不能爲它使用==和!=。

⑦如果被約束成引用類型,那麼具體執行的比較將完全取決於類型參數被約束成什麼類型。如果它只是一個引用類型,那麼執行的是簡單的引用比較。如果它被進一步約束成繼承自某個重載了==和!=操作符的特定類型,就會使用重載的操作符。但要注意,假如調用者指定的類型實參恰巧也進行了重載,那麼這個重載操作符是不會使用的

⑧遇到泛型類型時,編譯器會在編譯未綁定的泛型類型時就解析好所有方法重載,而不是等到執行時,纔去爲每個可能的方法調用重新考慮是否存在更具體的重載

3、泛型比較接口

共有4個主要的泛型接口可用於比較。IComparer和IComparable用於排序(判斷某個值是小於、等於還是大於另一個值),而IEqualityComparer和IEquatable通過某種標準來比較兩個項的相等性,或查找某個項的散列(通過與相等性概念匹配的方式)

如果換一種方式來劃分這4個接口,IComparaer和IEqualityComparer的實例能夠比較兩個不同的值,而IComparable和<TIEquatable的實例則可以比較它們本身和其他值

4、高級泛型

1、靜態字段和靜態構造函數

就像實例字段從屬於一個實例一樣,靜態字段從屬於聲明它們的類型,因而每個封閉類型都有它自己的靜態字段集。同樣的規則也適用於靜態初始化器和靜態構造函數。雖然一個泛型類型可能嵌套在另一個泛型類型中,而且一個類型可能有多個泛型參數,但是它和非泛型類型一樣,每個不同的類型實參列表都被看做一個不同的封閉類型。

2、JIT編譯器如何處理泛型

對於所有不同的封閉類型,JIT的職責就是將泛型類型的IL轉換成本地代碼,使其能真正運行起來。JIT爲每個以值類型作爲類型實參的封閉類型都創建不同的代碼。然而,所有使用引用類型作爲類型實參的封閉類型都共享相同的本地代碼。之所以能這樣做,是由於所有引用都具有相同的大小(32位CLR上是4字節,64位CLR上是8字節)。無論實際引用的是什麼,引用構成的數組的大小是不會發生變化的,棧上一個引用所需的空間始終是相同的。

image-20201021204622044

在.NET 1.1中,爲了將單獨的字節添加到一個ArrayList中,需要對每個字節進行裝箱,並存儲對每個已裝箱值的引用。使用List則無此問題——List用一個T[]類型的成員數組替代了ArrayList中的object[],而且那個數組具有恰當的類型,會佔用恰當(大小)的空間。上圖展示了一個ArrayList和一個List,它們分別包含6個相同的值。數組本身擁有不止6個元素,從而允許擴充。List和ArrayList都有一個緩衝區,在必要時會創建一個更大的緩衝區。

假定ArrayList使用的是一個32位CLR,每個已裝箱的字節都要產生8字節的對象開銷,另加4字節用於數據本身。除此之外,引用本身也要消耗4字節。所以,每個有效數據都要花費至少16字節。除此之外,緩衝區中還要爲引用準備一些額外的未使用的空間。List中的每個字節都佔用元素數組中一個字節的空間。緩衝區仍有“浪費”的空間,可用於新增項,但最起碼,每個未使用的元素只會浪費一個字節。不僅節省了空間,而且加快了執行速度。現在不需要花時間進行裝箱,不需要因爲對字節進行拆箱而檢查類型,也不需要對不再引用的已裝箱值進行垃圾回收。

3、泛型迭代

對集合執行的最常見的操作之一就是遍歷(迭代)它的所有元素。最簡單的辦法就是使用foreach語句。在C# 1中,爲了使用foreach,集合要麼必須實現System.Collections. IEnumerable接口,要麼必須有一個類似的GetEnumerator()方法。C# 2使過程變得容易了一些。foreach語句的規則得到了擴展,現在還可以使用System. Collections.Generic.IEnumerable接口及其搭檔IEnumerator

在少數情況下,當需要爲自己的某個類型實現迭代時,你會發現由於IEnumerable擴展了舊的IEnumerable接口,所以要實現兩個不同的方法:IEnumerator GetIEnumerator()和IEnumerator GetIEnumerator();如果使用“顯式接口實現”來實現IEnumerable,就可以用一個“普通”的方法來實現IEnumerable。幸好,由於IEnumerator擴展了IEnumerator,所以兩個方法可以使用相同的返回值,而且只需調用一下泛型版本,就可實現非泛型方法。

上述兩個方法只是返回類型不同,而根據C#的重載規則,一般不允許寫這樣的兩個方法。這樣做的目的是基於一個基本原則:如果沒有問題,泛型接口都應該繼承對應的非泛型接口,這樣可以實現協變性。例如,假如以前爲.NET 1.1寫的一個函數要獲取IEnumerable類型的參數,而現在有了IEnumerable。假如IEnumerable不繼承IEnumerable,這個函數就不能接受IEnumerable類型的參數

4、泛型和反射

1、對泛型類型使用typeof:可通過兩種方式作用於泛型類型,一種方式是獲取泛型類型定義,另一種方式是獲取特定的已構造類型。爲了獲取泛型類型定義,需要提供聲明的類型名稱,刪除所有類型參數名稱,但保留逗號;爲了獲取已構造類型,需要採取與聲明泛型類型變量時相同的方式指定類型實參就可以了。示例:

image-20201021210758593

2、System.Type的屬性和方法:新增的許多新的方法和屬性中有兩個方法是最重要的GetGenericTypeDefinition和MakeGenericType。兩個方法所執行的操作實際上是相反的——第一個作用於已構造的類型,獲取它的泛型類型定義;第二個作用於泛型類型定義,返回一個已構造類型。示例:

image-20201021211041544

3、反射泛型方法:首先獲取泛型方法定義,然後使用MakeGenericMethod返回一個已構造的泛型方法。從泛型類型定義獲取的方法不能直接調用——相反,必須從一個已構造的類型獲取方法。無論泛型方法還是非泛型方法,這一點都適用


總結:

泛型的3個主要優點:編譯時的類型安全性、性能和代碼的表現力。

值類型在性能上的獲益是最大的。在強類型的泛型API中使用時,它們不再需要裝箱和拆箱。引用類型的性能也有所提升,只是幅度較小。

使用泛型,代碼能更清楚地表達其意圖,而不是隻能通過一條註釋或者一個很長的變量名來準確地描述涉及的類型。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章