# [C# 基礎知識系列]專題八: 深入理解泛型(二)
**引言:**
本專題主要是承接上一個專題要繼續介紹泛型的其他內容,這里就不多說了,就直接進入本專題的內容的。
**一、類型推斷**
在我們寫泛型代碼的時候經常有大量的"<"和">"符號,這樣有時候代碼一多,也難免會讓開發者在閱讀代碼過程中會覺得有點暈的,此時我們覺得暈的時候肯定就會這樣想:是不是能夠省掉一些"<" 和">"符號的呢?你有這種需求了, 當然微軟這位好人肯定也會幫你解決問題的,這樣就有了我們這部分的內容——**類型推斷**,意味著編譯器會在調用一個泛型方法時自動判斷要使用的類型,(這里要注意的是:類型推斷只使用于泛型方法,不適用于泛型類型),下面是演示代碼:
```
using System;
namespace 類型推斷例子
{
class Program
{
static void Main(string[] args)
{
int n1 = 1;
int n2 = 2;
// 沒有類型推斷時需要寫的代碼
// GenericMethodTest<int>(ref n1, ref n2);
// 有了類型推斷后需要寫的代碼
// 此時編譯器可以根據傳遞的實參 1和2來判斷應該使用Int類型實參來調用泛型方法
// 可以看出有了類型推斷之后少了<>,這樣代碼多的時候可以增強可讀性
GenericMethodTest(ref n1, ref n2);
Console.WriteLine("n1的值現在為:" + n1);
Console.WriteLine("n2的值現在為:" + n2);
Console.Read();
//string t1 = "123";
//object t2 = "456";
//// 此時編譯出錯,不能推斷類型
//// 使用類型推斷時,C#使用變量的數據類型,而不是使用變量引用對象的數據類型
//// 所以下面的代碼會出錯,因為C#編譯器發現t1是string,而t2是一個object類型
//// 即使 t2引用的是一個string,此時由于t1和t2是不同數據類型,編譯器所以無法推斷出類型,所以報錯。
//GenericMethodTest(ref t1, ref t2);
}
// 類型推斷的Demo
private static void GenericMethodTest<T>(ref T t1,ref T t2)
{
T temp = t1;
t1 = t2;
t2 = temp;
}
}
}
```
代碼中都有詳細的注釋,這里就不解釋了。
**二、類型約束**
如果大家看了我的上一個專題的話,就應該會注意到我在實現泛型類的時候用到了where T : IComparable,在上一個專題并沒有和大家介紹這個是泛型的什么用法,這個用法就是這個部分要講的類型約束,其實where T : IComparable這句代碼也很好理解的,猜猜也明白的(如果是我不知道的話,應該是猜類型參數T要滿足IComparable這個接口條件,因為Where就代表符合什么條件的意思,然而真真意思也確實如此的)下面就讓我們具體看看泛型中的類型參數有哪幾種約束的。 首先,編譯泛型代碼時,C#編譯器肯定會對代碼進行分析,如果我們像下面定義一個泛型類型方法時,編譯器就會報錯:
```
// 比較兩個數的大小,返回大的那個
private static T max<T>(T obj1, T obj2)
{
if (obj1.CompareTo(obj2) > 0)
{
return obj1;
}
return obj2;
}
```
如果像上面一樣定義泛型方法時,C#編譯器會提示錯誤信息:“T”不包含“CompareTo”的定義,并且找不到可接受類型為“T”的第一個參數的擴展方法“CompareTo”。 這是因為此時類型參數T可以為任意類型,然而許多類型都沒有提供CompareTo方法,所以C#編譯器不能編譯上面的代碼,這時候我們(_編譯器也是這么想的_)肯定會想——如果C#編譯器知道類型參數T有CompareTo方法的話,這樣上面的代碼就可以被C#編譯器驗證的時候通過,就不會出現編譯錯誤的(C#編譯器感覺很人性化的,都會按照人的思考方式去解決問題的,那是因為編譯器也是人開發出來的,當然會人性化的,因為開發人員當時就是這么想的,所以就把邏輯寫到編譯器的實現中去了),這樣就讓我們想對類型參數作出一定約束,縮小類型參數所代表的類型數量——這就是我們類型約束的目的,從而也很自然的有了**類型參數約束**(**這里通過對遇到的分析然后去想辦法的解決的方式來引出類型約束的概念,主要是讓大家可以明白C#中的語言特性提出來都是有原因,并不是說微軟想提出來就提出來的,主要還是因為用戶會有這樣的需求,這樣的方式我覺得可以讓大家更加的明白C#語言特性的發展歷程,從而更加深入理解C#,從我前面的專題也看的出來我這樣介紹問題的方式的,不過這樣也是我個人的理解,希望這樣引入問題的方式對大家會有幫助,讓大家更好的理解C#語言特性,如果大家對于對于有任何意見和建議的話,都可以在留言中提出的,如果覺得好的話,也麻煩表示認可下**)。所以上面的代碼可以指定一個類型約束,讓C#編譯器知道這個類型參數一定會有CompareTo方法的,這樣編譯器就不會報錯了,我們可以將上面代碼改為(代碼中T:IComparable<T>為類型參數T指定的類型實參都必須實現泛型**IComparable接口**):
```
// 比較兩個數的大小,返回大的那個
private static T max<T>(T obj1, T obj2) **where T:IComparable<T>**
{
if (obj1.CompareTo(obj2) > 0)
{
return obj1;
}
return obj2;
}
```
類型約束就是用**where** 關鍵字來限制能**指定類型實參**的類型數量,如上面的where T:IComparable<T>語句。C# 中有4種約束可以使用,然而這4種約束的語法都差不多。(約束要放在泛型方法或泛型類型聲明的末尾,并且要使用Where關鍵字)
(1) 引用類型約束
表示形式為 **T:class**, 確保傳遞的類型實參必須是引用類型(注意約束的類型參數和類型本身沒有關系,意思就是說定義一個泛型結構體時,泛型類型一樣可以約束為引用類型,此時結構體類型本身是值類型,而類型參數約束為引用類型),可以為任何的類、接口、委托或數組等;但是注意不能指定下面特殊的引用類型:**System.Object,System.Array,System.Delegate,System.MulticastDelegate,System.ValueType,System.Enum和System.Void.**
如下面定義的泛型類:
```
using System.IO;
public class samplereference<T> where T : Stream
{
public void Test(T stream)
{
stream.Close();
}
}
```
上面代碼中類型參數T設置了引用類型約束,**Where T:stream**的意思就是告訴編譯器,傳入的類型實參必須是System.IO.Stream或者從Stream中派生的一個類型,如果一個類型參數沒有指定約束,則默認T為**System.Object**類型(相當于一個默認約束一樣,就想每個類如果沒有指定構造函數就會有默認的無參數構造函數,如果指定了帶參數的構造函數,編譯器就不會生成一個默認的構造函數)。然而,如果我們在代碼中顯示指定System.Object約束時,此時會編譯器會報錯:**約束不能是特殊類“object**”(這里大家可以自己試試看的)
(2)值類型約束
表示形式為**T:struct**,確保傳遞的類型實參時值類型,其中包括枚舉,但是可空類型排除,(可空類型將會在后面專題有所介紹),如下面的示例:
```
// 值類型約束
public class samplevaluetype<T> where T : struct
{
public static T Test()
{
return new T();
}
}
```
在上面代碼中,**new T()**是可以通過編譯的,因為T 是一個值類型,而所有值類型都有一個公共的無參構造函數,然而,如果T不約束,或約束為引用類型時,此時上面的代碼就會報錯,因為有的引用類型沒有公共的無參構造函數的。
(3)構造函數類型約束
表示形式為**T:new(),**如果類型參數有多個約束時,此約束必須為最后指定。確保指定的類型實參有一個公共無參構造函數的非抽象類型,這適用于:所有值類型;所有非靜態、非抽象、沒有顯示聲明的構造函數的類(前面括號中已經說了,如果顯示聲明帶參數的構造函數,則編譯器就不會為類生成一個默認的無參構造函數,大家可以通過IL反匯編程序查看下的,這里就不貼圖了);顯示聲明了一個公共無參構造函數的所有非抽象類。(注意: 如果同時指定構造器約束和struct約束,C#編譯器會認為這是一個錯誤,因為這樣的指定是多余的,所有值類型都隱式提供一個無參公共構造函數,就如定義接口指定訪問類型為public一樣,編譯器也會報錯,因為接口一定是public的,這樣的做只多余的,所以會報錯。)
(4)轉換類型約束
表示形式為 **T:基類名** (確保指定的類型實參必須是基類或派生自基類的子類)**或T:接口名**(確保指定的類型實參必須是接口或實現了該接口的類) **或T:U**(為 T 提供的類型參數必須是為 U 提供的參數或派生自為 U 提供的參數)。轉換約束的例子如下:
| 聲明 | 已構造類型的例子 |
| --- | --- |
| Class Sample<T> where T: Stream | Sample<Stream>有效的Sample<string>無效的 |
| Class Sample<T> where T: IDisposable | Sample<Stream >有效的Sample<StringBuilder>無效的 |
| Class Sample<T,U> where T: U | Sample<Stream,IDispsable>有效的Sample<string,IDisposable>無效的 |
(5)組合約束(第五種約束就是前面的4種約束的組合)
將多個不同種類的約束合并在一起的情況就是組合約束了。(注意,沒有任何類型即時引用類型又是值類型的,所以引用約束和值約束不能同時使用)如果存在多個轉換類型約束時,如果其中一個是類,則類必須放在接口的前面。不同的類型參數可以有不同的約束,但是他們分別要由一個單獨的where關鍵字。下面看一些有效和無效的例子來讓大家加深印象:
有效:
class Sample<T> where T:class, IDisposable, new();
class Sample<T,U> where T:class where U: struct
**無效的:**
class Sample<T> where T: class, struct (沒有任何類型即時引用類型又是值類型的,所以為無效的)
class Sample<T> where T: Stream, class (引用類型約束應該為第一個約束,放在最前面,所以為無效的)
class Sample<T> where T: new(), Stream (構造函數約束必須放在最后面,所以為無效)
class Sample<T> where T: IDisposable, Stream(類必須放在接口前面,所以為無效的)
class Sample<T,U> where T: struct where U:class, T (類型形參“T”具有“struct”約束,因此“T”不能用作“U”的約束,所以為無效的)
class Sample<T,U> where T:Stream, U:IDisposable(不同的類型參數可以有不同的約束,但是他們分別要由一個單獨的where關鍵字,所以為無效的)
**三、利用反射調用泛型方法**
下面就直接通過一個例子來演示如何利用反射來動態調用泛型方法的(關于反射的內容可以我博客中的這篇文章: [http://www.cnblogs.com/zhili/archive/2012/07/08/AssemblyLoad_and_Reflection.html](http://www.cnblogs.com/zhili/archive/2012/07/08/AssemblyLoad_and_Reflection.html)),演示代碼如下:
```
using System;
using System.Reflection;
namespace ReflectionGenericMethod
{
class Program
{
static void Main(string[] args)
{
Test test = new Test();
Type type = test.GetType();
// 首先,獲得方法的定義
// 如果不傳入BindFlags實參,GetMethod方法只返回公共成員
// 這里我指定了NonPublic,也就是返回私有成員
// (這里要注意的是,如果指定了Public或NonPublic的話,
// 必須要同時指定Instance|Static,否則不返回成員,具體大家可以用代碼來測試的)
MethodInfo methodefine = type.GetMethod("PrintTypeParameterMethod", BindingFlags.NonPublic|BindingFlags.Instance|BindingFlags.Static);
MethodInfo constructed;
// 使用MakeGenericMethod方法來獲得一個已構造的泛型方法
constructed = methodefine.MakeGenericMethod(typeof(string));
// 泛型方法的調用
constructed.Invoke(null,null);
Console.Read();
}
}
public class Test
{
private static void PrintTypeParameterMethod<T>()
{
Console.WriteLine(typeof(T));
}
}
}
```
上面代碼在調用泛型方法時傳入的兩個實參都是null,傳入第一個為null是因為調用的是一個靜態方法, 第二null是因為調用的方法是個無參的方法。 運行結果截圖(結果是輸出出 類型實參的類型,結果和我們預期的一樣):

**四、小結**
說到這里泛型的內容都已經介紹完了,本系列用了三個專題來介紹泛型,文章內容都基本采用提出疑問(為什么有泛型)到解釋疑問,再到深入理解泛型的方式(個人認為這樣的講解方式不錯的,如果大家有更好的講解方式可以在下面留言給我),希望這種方式可以讓大家知道泛型的起源,從而更好的理解泛型。后面一專題將和大家介紹了C#4.0中對泛型的改進——**泛型的可變性**。
泛型專題中用到的所有Demo的源代碼:[http://files.cnblogs.com/zhili/GeneralDemo.zip](http://files.cnblogs.com/zhili/GeneralDemo.zip)
- C# 基礎知識系列
- C# 基礎知識系列 專題一:深入解析委托——C#中為什么要引入委托
- C# 基礎知識系列 專題二:委托的本質論
- C# 基礎知識系列 專題三:如何用委托包裝多個方法——委托鏈
- C# 基礎知識系列 專題四:事件揭秘
- C# 基礎知識系列 專題五:當點擊按鈕時觸發Click事件背后發生的事情
- C# 基礎知識系列 專題六:泛型基礎篇——為什么引入泛型
- C# 基礎知識系列 專題七: 泛型深入理解(一)
- C# 基礎知識系列 專題八: 深入理解泛型(二)
- C# 基礎知識系列 專題九: 深入理解泛型可變性
- C#基礎知識系列 專題十:全面解析可空類型
- C# 基礎知識系列 專題十一:匿名方法解析
- C#基礎知識系列 專題十二:迭代器
- C#基礎知識 專題十三:全面解析對象集合初始化器、匿名類型和隱式類型
- C# 基礎知識系列 專題十四:深入理解Lambda表達式
- C# 基礎知識系列 專題十五:全面解析擴展方法
- C# 基礎知識系列 專題十六:Linq介紹
- C#基礎知識系列 專題十七:深入理解動態類型
- 你必須知道的異步編程 C# 5.0 新特性——Async和Await使異步編程更簡單
- 全面解析C#中參數傳遞
- C#基礎知識系列 全面解析C#中靜態與非靜態
- C# 基礎知識系列 C#中易混淆的知識點
- C#進階系列
- C#進階系列 專題一:深入解析深拷貝和淺拷貝
- C#進階系列 專題二:你知道Dictionary查找速度為什么快嗎?
- C# 開發技巧系列
- C# 開發技巧系列 使用C#操作Word和Excel程序
- C# 開發技巧系列 使用C#操作幻燈片
- C# 開發技巧系列 如何動態設置屏幕分辨率
- C# 開發技巧系列 C#如何實現圖片查看器
- C# 開發技巧 如何防止程序多次運行
- C# 開發技巧 實現屬于自己的截圖工具
- C# 開發技巧 如何使不符合要求的元素等于離它最近的一個元素
- C# 線程處理系列
- C# 線程處理系列 專題一:線程基礎
- C# 線程處理系列 專題二:線程池中的工作者線程
- C# 線程處理系列 專題三:線程池中的I/O線程
- C# 線程處理系列 專題四:線程同步
- C# 線程處理系列 專題五:線程同步——事件構造
- C# 線程處理系列 專題六:線程同步——信號量和互斥體
- C# 多線程處理系列專題七——對多線程的補充
- C#網絡編程系列
- C# 網絡編程系列 專題一:網絡協議簡介
- C# 網絡編程系列 專題二:HTTP協議詳解
- C# 網絡編程系列 專題三:自定義Web服務器
- C# 網絡編程系列 專題四:自定義Web瀏覽器
- C# 網絡編程系列 專題五:TCP編程
- C# 網絡編程系列 專題六:UDP編程
- C# 網絡編程系列 專題七:UDP編程補充——UDP廣播程序的實現
- C# 網絡編程系列 專題八:P2P編程
- C# 網絡編程系列 專題九:實現類似QQ的即時通信程序
- C# 網絡編程系列 專題十:實現簡單的郵件收發器
- C# 網絡編程系列 專題十一:實現一個基于FTP協議的程序——文件上傳下載器
- C# 網絡編程系列 專題十二:實現一個簡單的FTP服務器
- C# 互操作性入門系列
- C# 互操作性入門系列(一):C#中互操作性介紹
- C# 互操作性入門系列(二):使用平臺調用調用Win32 函數
- C# 互操作性入門系列(三):平臺調用中的數據封送處理
- C# 互操作性入門系列(四):在C# 中調用COM組件
- CLR
- 談談: String 和StringBuilder區別和選擇
- 談談:程序集加載和反射
- 利用反射獲得委托和事件以及創建委托實例和添加事件處理程序
- 談談:.Net中的序列化和反序列化
- C#設計模式
- UML類圖符號 各種關系說明以及舉例
- C#設計模式(1)——單例模式
- C#設計模式(2)——簡單工廠模式
- C#設計模式(3)——工廠方法模式
- C#設計模式(4)——抽象工廠模式
- C#設計模式(5)——建造者模式(Builder Pattern)
- C#設計模式(6)——原型模式(Prototype Pattern)
- C#設計模式(7)——適配器模式(Adapter Pattern)
- C#設計模式(8)——橋接模式(Bridge Pattern)
- C#設計模式(9)——裝飾者模式(Decorator Pattern)
- C#設計模式(10)——組合模式(Composite Pattern)
- C#設計模式(11)——外觀模式(Facade Pattern)
- C#設計模式(12)——享元模式(Flyweight Pattern)
- C#設計模式(13)——代理模式(Proxy Pattern)
- C#設計模式(14)——模板方法模式(Template Method)
- C#設計模式(15)——命令模式(Command Pattern)
- C#設計模式(16)——迭代器模式(Iterator Pattern)
- C#設計模式(17)——觀察者模式(Observer Pattern)
- C#設計模式(18)——中介者模式(Mediator Pattern)
- C#設計模式(19)——狀態者模式(State Pattern)
- C#設計模式(20)——策略者模式(Stragety Pattern)
- C#設計模式(21)——責任鏈模式
- C#設計模式(22)——訪問者模式(Vistor Pattern)
- C#設計模式(23)——備忘錄模式(Memento Pattern)
- C#設計模式總結
- WPF快速入門系列
- WPF快速入門系列(1)——WPF布局概覽
- WPF快速入門系列(2)——深入解析依賴屬性
- WPF快速入門系列(3)——深入解析WPF事件機制
- WPF快速入門系列(4)——深入解析WPF綁定
- WPF快速入門系列(5)——深入解析WPF命令
- WPF快速入門系列(6)——WPF資源和樣式
- WPF快速入門系列(7)——深入解析WPF模板
- WPF快速入門系列(8)——MVVM快速入門
- WPF快速入門系列(9)——WPF任務管理工具實現
- ASP.NET 開發
- ASP.NET 開發必備知識點(1):如何讓Asp.net網站運行在自定義的Web服務器上
- ASP.NET 開發必備知識點(2):那些年追過的ASP.NET權限管理
- ASP.NET中實現回調
- 跟我一起學WCF
- 跟我一起學WCF(1)——MSMQ消息隊列
- 跟我一起學WCF(2)——利用.NET Remoting技術開發分布式應用
- 跟我一起學WCF(3)——利用Web Services開發分布式應用
- 跟我一起學WCF(3)——利用Web Services開發分布式應用
- 跟我一起學WCF(4)——第一個WCF程序
- 跟我一起學WCF(5)——深入解析服務契約 上篇
- 跟我一起學WCF(6)——深入解析服務契約 下篇
- 跟我一起學WCF(7)——WCF數據契約與序列化詳解
- 跟我一起學WCF(8)——WCF中Session、實例管理詳解
- 跟我一起學WCF(9)——WCF回調操作的實現
- 跟我一起學WCF(10)——WCF中事務處理
- 跟我一起學WCF(11)——WCF中隊列服務詳解
- 跟我一起學WCF(12)——WCF中Rest服務入門
- 跟我一起學WCF(13)——WCF系列總結
- .NET領域驅動設計實戰系列
- .NET領域驅動設計實戰系列 專題一:前期準備之EF CodeFirst
- .NET領域驅動設計實戰系列 專題二:結合領域驅動設計的面向服務架構來搭建網上書店
- .NET領域驅動設計實戰系列 專題三:前期準備之規約模式(Specification Pattern)
- .NET領域驅動設計實戰系列 專題四:前期準備之工作單元模式(Unit Of Work)
- .NET領域驅動設計實戰系列 專題五:網上書店規約模式、工作單元模式的引入以及購物車的實現
- .NET領域驅動設計實戰系列 專題六:DDD實踐案例:網上書店訂單功能的實現
- .NET領域驅動設計實戰系列 專題七:DDD實踐案例:引入事件驅動與中間件機制來實現后臺管理功能
- .NET領域驅動設計實戰系列 專題八:DDD案例:網上書店分布式消息隊列和分布式緩存的實現
- .NET領域驅動設計實戰系列 專題九:DDD案例:網上書店AOP和站點地圖的實現
- .NET領域驅動設計實戰系列 專題十:DDD擴展內容:全面剖析CQRS模式實現
- .NET領域驅動設計實戰系列 專題十一:.NET 領域驅動設計實戰系列總結