30分鐘泛型教程

發表於2013-05-03

來源:liulun 的部落格

一、泛型入門:

我們先來看一個最為常見的泛型型別List<T>的定義
(真正的定義比這個要複雜的多,我這裡刪掉了很多東西)

List後面緊跟著一個<T>表示它操作的是一個未指定的資料型別
(T代表著一個未指定的資料型別)

可以把T看作一個變數名,T代表著一個型別,
在List<T>的原始碼中任何地方都能使用T

T被用作方法的引數和返回值
Add方法接收T型別的引數,ToArray方法返回一個T型別的陣列

注意:

泛型引數必須以T開頭,要麼就叫T,要麼就叫TKey或者TValue;
這跟介面要以I開頭是一樣的,這是約定。

下面來看一段使用泛型型別的程式碼

請注意上面程式碼裡的註釋

二、泛型的作用(1):

作為程式設計師,寫程式碼時刻不忘程式碼重用。
程式碼重用可以分成很多類,其中演算法重用就是非常重要的一類

假設你要為一組整型資料寫一個排序演算法,又要為一組浮點型資料寫一個排序演算法
如果沒有泛型型別,你會怎麼做呢?

你可能想到了方法的過載
寫兩個同名方法,一個方法接收整型陣列,另一個方法接收浮點型的陣列

但有了泛型,你就完全不必這麼做,只要設計一個方法就夠用了,你甚至可以用這個方法為一組字串資料排序

三、泛型的作用(2):

假設你是一個方法的設計者,
這個方法需要有一個輸入引數,但你並能確定這個輸入引數的型別
那麼你會怎麼做呢?

有一部分人可能會馬上反駁:“不可能有這種時候!”
那麼我會跟你說,程式設計是一門經驗型的工作,你的經驗還不夠,還沒有碰到過類似的地方。

另一部分人可能考慮把這個引數的型別設定成Object的
這確實是一種可行的方案
但會造成下面兩個問題

如果我給這個方法傳遞整形的資料
(值型別的資料都一樣)
就會產生額外的裝箱、拆箱操作
造成效能損耗

如果你這個方法裡的處理邏輯不適用於字串的引數
而使用者又傳了一個字串進來
編譯器是不會報錯的,
只有在執行期才會報錯
(如果質管部門沒有測出這個執行期BUG,那麼不知道要造成多大的損失呢)
這就是我們常說的:型別不安全

四、泛型的示例:

像List<T>和Dictionary<TKey,TValue>之類的泛型型別我們經常用到
下面我介紹幾個不常用到的泛型型別

ObservableCollection<T>
當這個集合發生改變後會有相應的事件得到通知
請看如下程式碼:

使用這個集合需要引用如下兩個名稱空間

 

BlockingCollection<int>是執行緒安全的集合
來看看下面這段程式碼

輸出結果為:

BlockingCollection<int>還可以設定CompleteAdding和IsCompleted屬性來拒絕加入新元素
.NET類庫還提供了很多的泛型型別,在這裡就不一一例舉了

五、泛型的繼承:

在.net中一切都繼承字Object
泛型也不例外
泛型型別可以繼承自其他型別
來看一下如下程式碼

泛型型別MyOtherType<T>成功的重寫了非泛型型別MyType的方法
如果我試圖按如下方式從MyOtherType<T>型別派生子型別就會導致編譯器錯誤

但是如果寫成這種方式,就不會出錯

注意:

如果按照如上寫法,會造成型別不統一的問題
如果一個方法接收MyThirdType型別的引數,
那麼不能將一個MyOtherType<int>的例項傳遞給這個方法
然而一個方法如果接收MyOtherType<int>型別的引數
卻可以把MyThirdType型別的例項傳遞給這個方法
這是CLR內部實現機制造成的
這看起來確實很怪異!

寫成如下方式也不會出錯

此中訣竅,只可意會,不可言傳

六、泛型介面

.NET類庫裡有很多泛型的介面
比如:IEnumerator<T>、IList<T>等
這裡不對這些介面做詳細描述了
值說說為什麼要有泛型介面。

其實泛型介面出現的原因和泛型出現的原因類似
拿IComparable這個介面來說,
此介面只描述了一個方法:

大家看到,如果是值型別的引數,勢必會導致裝箱和拆箱操作
同時,也不是強型別的,不能在編譯期確定引數的型別
有了IComparable<T>就解決掉這個問題了

七、泛型委託

委託描述方法,
泛型委託的由來和泛型介面類似

定義一個泛型委託也比較簡單:

這個委託描述一類方法
這類方法接收T型別的引數,沒有返回值
來看看使用這個委託的方法

由於定義委託比較繁瑣
.NET類庫在System名稱空間,下定義了三種比較常用的泛型委託

Predicate<T>委託:

這個委託描述的方法為接收一個T型別的引數,返回一個BOOL型別的值,一般用於比較方法

Action<T>委託

這個委託描述的方法,接收一個或多個T型別的引數(最多16個,我這裡只寫了兩種型別的定義方式),沒有返回值

Func<T>委託

這個委託描述的方法,接收零個或多個T型別的引數(最多16個,我這裡只寫了兩種型別的定義方式),
與Action委託不同的是,它有一個返回值,返回值的型別為TResult型別的

關於委託的描述,您還可以看我這篇文章
30分鐘LINQ教程

八、泛型方法

泛型型別中的T可以用在這個型別的任何地方
然而有些時候,我們不希望在使用型別的時候就指定T的型別
我們希望在使用這個型別的方法時,再指定T的型別
來看看如下程式碼:

上面的程式碼中MyClass並不是一個泛型型別
但這個型別中的CompareTo<TParam>()卻是一個泛型方法
TParam可以用在這個方法中的任何地方。

使用泛型方法一般用如下程式碼就可以了:

然而,你可以寫的更簡單一些,寫成如下的方式

有人會問:“這不可能,沒有指定CompareTo方法的TParam型別,肯定會編譯出錯的”
我告訴你:不會的,編譯器可以幫你完成型別推斷的工作。

注意:
如果你為一個方法指定了兩個泛型引數,而且這兩個引數的型別都是T,
那麼如果你想使用型別推斷,你必須傳遞兩個相同型別的引數給這個方法
不能一個引數用string型別,另一個用object型別,這會導致編譯錯誤。

九、泛型約束

我們設計了一個泛型型別
很多時候,我們不希望使用者傳入任意型別的引數
也就是說,我們希望“約束”一下T的型別
來看看如下程式碼:

上面的程式碼要求T型別必須實現了IComparable<T>介面
如你所見:泛型的約束通過關鍵字where來實現。

泛型方法當然也可以通過類似的方式對泛型引數進行約束
請看如下程式碼

上面程式碼中用了class關鍵字約束泛型引數TParam;具體稍後解釋。

注意1:
如果我有一個型別也定義為MyClass<T>但沒有做約束,
那麼這個時候,做過約束的MyClass<T>將與沒做約束的MyClass<T>衝突,編譯無法通過

注意2:
當你重寫一個泛型方法時,如果這個方法指定了約束
在重寫這個方法時,不能再指定約束了

注意3:
雖然我上面的例子寫的是介面約束,但你完全可以寫一個型別,比如說BaseClass
而且,只要是繼承自BaseClass的型別都可以當作T型別使用,你不要試圖約束T為Object型別,編譯不會通過的。(傻子才這麼幹)

注意4:
有兩個特殊的約束:class和struct。
where T : class   約束T型別必須為引用型別
where T : struct  約束T型別必須為值型別

注意5:
如果你沒有對T進行class約束,
那麼你不能寫這樣的程式碼:T obj = null;  這無法通過編譯,因為T有可能是值型別的。
如果你沒有對T進行struct約束,也沒有對T進行new約束
那麼你不能寫這樣的程式碼:T obj = new T();  這無法通過編譯,因為值型別肯定有無引數構造器,而引用型別就不一定了。
如果你對T進行了new約束:where T : new();  那麼new T()就是正確的,因為new約束要求T型別有一個公共無參構造器。

注意6:
就算沒有對T進行任何約束,也有一個辦法來處理值型別和引用型別的問題
T temp = default(T);
如果T為引用型別,那麼temp就是null;如果T為值型別,那麼temp就是0;

注意7:
試圖對T型別的變數進行強制轉化,一般情況下會報編譯期錯誤。
但你可以先把T轉化成object再把object轉化成你要的型別(一般不推薦這麼做,你應該考慮把T轉化成一個約束相容的型別)
你也可以考慮用as操作符進行型別轉化,這一般不會報錯,但只能轉化成引用型別。

關於泛型約束的內容,我在這篇文章裡也有提到
30分鐘linq教程

十、逆變和協變

一般情況下,我們使用泛型時,由T標記的泛型型別是不能更改的
也就是說,如下兩種寫法都是錯誤的

注意:這裡沒有寫強制轉換,即使寫了強制轉換也是錯誤的,編譯就無法通過

然而泛型提供了逆變和協變的特性,
有了這兩種特性,這種轉換就成為了可能。

逆變:
泛型型別T可以從基型別更改為該類的派生型別,
用in關鍵字標記逆變形式的型別引數,
而且這個引數一般作輸入引數。

協變:
泛型型別T可以從派生型別更改為它的基型別,
用out關鍵字來標記協變形式的型別引數,
而且這個引數一般作為返回值

如果我們定義了一個這樣的委託:

那麼,就可以讓如下程式碼通過編譯(不用強制轉換)

這就是逆變和協變的威力。

參考資料

Mgen的部落格
CLR VIA C#(第三版)

修改記錄:

2013.4.21完成了一半的內容

2013.4.28完成了全部內容,修改了一些錯別字

2013.4.29增加了一大部分內容,修改了排版樣式

 

相關文章