# [C# 基礎知識系列] 專題十五:全面解析擴展方法
**引言:**
C# 3中所有特性的提出都是更好地為Linq服務的, 充分理解這些基礎特性后。對于更深層次地去理解Linq的架構方面會更加簡單,從而就可以自己去實現一個簡單的ORM框架的,對于Linq的學習在下一個專題中將會簡單和大家介紹下,這個專題還是先來介紹服務于Linq的基礎特性——擴展方法
**一、擴展方法的介紹**
我一般理解一個知識點喜歡拆分去理解,所以對于擴展方法的理解可以拆分為——首先它肯定是一個方法,然而方法又是對于一個類型而言的,所以擴展方法可以理解為**現有的類型(現有類型可以為自定義的類型和.Net 類庫中的類型)擴展(添加)應該附加到該類型中的方法**。
在沒有擴展方法之前,如果我們想為一個已有類型自定義自己邏輯的方法時,我們必須自定義一個新的類型來繼承已有類型的方式來添加方法,使用這種繼承方式來添加方法時,我們必須自定義一個新的派生類型,如果基類有抽象方法還需要重新去實現抽象方法,這樣為了擴展一個方法卻會導致因繼承而帶來的其他的開銷(指的是又要去自定義一個派生類,還要覆蓋基類的抽象方法等),所以使用繼承來為現有類型擴展方法時就有點大才小用的感覺了,并且當我們需要為值類型和密封類(不能被繼承的類)這些不能被繼承的類型擴展方法時,此時繼承就不能被我們所用了, 所以在C#3 中提出了用擴展方法來實現為現有類型添加方法。使用擴展方法來實現擴展可以解決使用繼承中所帶來的所有的弊端,下面通過一個例子來演示下擴展方法的使用:
```
class Program
{
/// <summary>
/// 擴展方法演示
/// </summary>
/// <param name="args"></param>
static void Main(string[] args)
{
#region 演示擴展方法的使用
// 調用擴展方法
WebRequest request = WebRequest.Create("http://www.cnblogs.com");
using (WebResponse response = request.GetResponse())
{
using(Stream responsestream =response.GetResponseStream())
{
using (FileStream output = File.Create("response.htm"))
{
// 調用擴展方法
responsestream.CopyToNewStream(output);
Console.Read();
}
}
}
#endregion
}
}
/// <summary>
/// 擴展方法必須在非泛型靜態類中定義
/// </summary>
public static class StreamExten
{
// 定義擴展方法
// 該擴展方法實現從一個流中內容復制到另一個流中
public static void CopyToNewStream(this Stream inputstream, Stream outputstream)
{
byte[] buffer = new byte[8192];
int read;
while ((read = inputstream.Read(buffer, 0, buffer.Length)) > 0)
{
outputstream.Write(buffer, 0, read);
}
}
}
```
上面程序中為Stream類型擴展了一個CopyToNewStream()的方法,然而從上面擴展方法的定義中大家可以知道擴展方法定義的一些規則,然而并不是所有方法都可以作為擴展方法來使用的, 此時朋友們就會問,我如何去分辨代碼中定義的是擴展方法還是普通的方法呢? 對于這個疑問,擴展方法的定義是要符合一些規則的,當看到定義的方法是符合這個規則,則就可以確定定義方法是擴展方法還是普通方法了。擴展方法必須具備下面的規則:
* 它必須在一個非嵌套、非泛型的靜態類中
* 它至少要有一個參數
* 第一個參數必須加上this關鍵字作為前綴(第一個參數類型也稱為擴展類型,即指方法對這個類型進行擴展)
* 第一個參數不能用其他任何修飾符(如不能使用ref out等修飾符)
* 第一個參數的類型不能是指針類型
對于上面的規則大家可以在代碼中試驗下就會很容易明白,這些規則是一些硬性的規定,如果違反了這些規則,編譯器可能會報錯或者說編譯器將不會認為定義的方法為擴展方法,下面簡單演示下擴展方法必須在非嵌套類型的靜態類中這個規則(其他規則同樣大家可以在代碼中進行測試),當我們把上面代碼中**StreamExten** 類定義為**Program**嵌套類型時,編譯器此時就會出現"擴展方法必須在頂級靜態類中定義;STreamExten是嵌套類"的編譯時錯誤,演示代碼如下:
View Code
```
class Program
{
/// <summary>
/// 擴展方法必須在非泛型靜態類中定義
/// </summary>
public static class StreamExten
{
// 定義擴展方法
// 該擴展方法實現從一個流中內容復制到另一個流中
public static void CopyToNewStream(this Stream inputstream, Stream outputstream)
{
byte[] buffer = new byte[8192];
int read;
while ((read = inputstream.Read(buffer, 0, buffer.Length)) > 0)
{
outputstream.Write(buffer, 0, read);
}
}
}
/// <summary>
/// 擴展方法演示
/// </summary>
/// <param name="args"></param>
static void Main(string[] args)
{
#region 演示擴展方法的使用
// 調用擴展方法
WebRequest request = WebRequest.Create("http://www.cnblogs.com");
using (WebResponse response = request.GetResponse())
{
using(Stream responsestream =response.GetResponseStream())
{
using (FileStream output = File.Create("response.htm"))
{
// 調用擴展方法
responsestream.CopyToNewStream(output);
Console.Read();
}
}
}
#endregion
}
}
```
下面是出現編譯時錯誤截圖:

**二、擴展方法是如何被發現的?**
從上面部分的介紹,朋友們應該知道了如何定義和使用一個擴展方法,并且從我們定義的規則中可以幫助我們開發人員更好地去識別擴展方法,知道程序中調用的是一個實例方法還是一個擴展方法,然而相信大家此時會有這樣一個疑問——編譯器是如何知道我調用的是一個擴展方法而不是一個該類中的一個實例方法呢?對于這個問題,將在這部分和大家分析下。
首先討論下程序員是如何去識別調用的是一個擴展方法而不是一個實例方法的,當我們看到調用方法的代碼時,首先我們會去找該方法是否是該類(如上面程序中的Stream類)的一個實例方法,進入**Stream**類(按F12進去查看)的定義中卻發現該類中沒有一個名為**CopyToNewStream**的方法,此時我們就會查看程序中是否定義了這樣的擴展方法,當找到一個為名CopyToNewStream這樣的方法時,然后再根據定義的規則來判斷找到的方法是否是為Stream類擴展的方法,這樣的一個過程就是我們程序員去發現一個擴展方法的過程,然而對于編譯器而言,它也是這么去發現擴展方法的(從而可以看出C#編譯器還是非常智能的,完全按照人的思路去思考問題,因為它也是人實現出來的,就當然是盡可能地去以人的思考方式去實現的了),下面就介紹下編譯器是如何去發現擴展方法的,這樣也可以與程序員們的思路進行對比下。
當編譯器看到變量調用的是一個方法時,它首先會去該對象中實例方法中去查看,一旦沒有找到與調用方法同名的實例方法時,編譯器就會去查找一個合適的擴展方法,它會檢查導入的所有命名空間和當前的命名空間中的所有擴展方法,并匹配變量類型到擴展類型存在一個隱式轉換的擴展方法。然而對于這個發現過程,可能有些人會問:編譯器如何知道某個方法是擴展方法而不是實例方法呢? 編譯器是根據System.Runtime.CompilerServices.ExtensionAttribute屬性來綁定方法是是否為擴展方法的, 當我們定義的方法是擴展方法時,該屬性會自動應用到方法上,編譯器還會將該特性應用到包含擴展方法的程序集上,對于這個兩點并不是我的推斷,下面給出反編譯截圖來證明下:

從上面編譯器發現擴展方法的過程可以得到方法調用的優先級的結論:現有的實例方法——>當前命名空間下的擴展方法——>導入命名空間的擴展方法。下面通過一個例子來演示編譯器的發現過程:
```
using System;
namespace 擴展方法如何被發現Demo
{
// 要使用不同命名空間的擴展方法首先要添加該命名空間的引用
using CustomNamesapce;
class Program
{
static void Main(string[] args)
{
Person p = new Person { Name = "Learning hard" };
// 當類型中包含了實例方法時,VS中的智能提示就只會列出實例方法,而不會列出擴展方法
// 當把實例方法注釋掉之后,VS的智能提示中才會列出擴展方法,此時編譯器在Person類型中找不到實例方法
// 所以首先從當前命名空間下查找是否有該名字的擴展方法,如果找到不會去其他命名空間中查找了
// 如果在當前命名空間中沒有找到,則會到導入的命名空間中再進行查找
p.Print();
p.Print("Hello");
Console.Read();
}
}
// 自定義類型
public class Person
{
public string Name { get; set; }
// 當類型中的實例方法
////public void Print()
////{
//// Console.WriteLine("調用實例方法輸出,姓名為: {0}", Name);
////}
}
// 當前命名空間下的擴展方法定義
public static class Extensionclass
{
/// <summary>
/// 擴展方法定義
/// </summary>
/// <param name="per"></param>
public static void Print(this Person per)
{
Console.WriteLine("調用的是同一命名空間下的擴展方法輸出,姓名為: {0}", per.Name);
}
}
}
namespace CustomNamesapce
{
using 擴展方法如何被發現Demo;
public static class CustomExtensionClass
{
/// <summary>
/// 擴展方法定義
/// </summary>
/// <param name="per"></param>
public static void Print(this Person per)
{
Console.WriteLine("調用的是不同命名空間下擴展方法輸出,姓名為: {0}", per.Name);
}
/// <summary>
/// 擴展方法定義
/// </summary>
/// <param name="per"></param>
public static void Print(this Person per,string s)
{
Console.WriteLine("調用的是不同命名空間下擴展方法輸出,姓名為: {0}, 附加字符串為{1}", per.Name, s);
}
}
}
```
運行結果:

當沒有注釋掉Person類中的實例方法Print時,此時在p后面鍵入.運算符時,智能提示將不會出現擴展方法(擴展方法前面有一個向下的箭頭標示出來的),下面是沒有注釋實例方法時智能提示的截圖(此時智能提示不會反射擴展方法出來):

并且從上面運行結果可以看出,當調用p.Print()方法時,此時調用的是離該調用較近的命名空間下的Print方法(盡管在CustomNamesapce命名空間下也定義了擴展方法Print)。、然而使用擴展方法還是存在一些問題的,如果同一個命名空間下的兩個類都含有擴展類型相同的方法時,此時編譯器就沒有辦法知道調用哪個方法了(這里標示出來引起大家的注意)。
**三、在空引用上調用方法**
大家都知道在C#中,在空引用上調用實例方法是會引發NullReferenceException異常的,但是可以在空引用上調用擴展方法,下面看一段演示代碼:
```
using System;
namespace 在空引用上調用方法Demo
{
// 必須引入擴展方法定義的命名空間
using ExtensionDefine;
class Program
{
static void Main(string[] args)
{
Console.WriteLine("空引用上調用擴展方法演示:");
string s = null;
// 在該程序中要使用擴展方法必須通過using來引用
// 在空引用上調用擴展方法不會發生NullReferenceException異常
// 之所以不會出現異常,是因為在空引用上調用擴展方法,對于編譯器而言只是把空引用s當成參數傳入靜態方法中而已
// 對于編譯器來說,s.IsNull()的調用等效于下面的代碼
//Console.WriteLine("字符串S為空字符串:{0}", NullExten.IsNull(s));
Console.WriteLine("字符串S為空字符串:{0}", s.IsNull());
Console.ReadKey();
}
}
}
namespace ExtensionDefine
{
/// <summary>
/// 擴展方法定義
/// </summary>
public static class NullExten
{
// 此時擴展的類型為object,這里我是故意用object類型的
// 如果是為了演示,當我們為一個類型定義擴展方法時,應盡量擴展具體類型,如果擴展其基類的話
// 則所有繼承于基類的類型都將具有該擴展方法,這樣對其他類型來說就進行了“污染
// 子所以形成了污染,是因為我們定義的擴展方法的意圖本來只想擴展某個子類。
// 其實下面這個方法我的意圖只是想擴展string類型的,所以更好的定義方法如下:
//public static bool isNull(this string str)
//{
// return str == null;
//}
// 不規范定義擴展方法的方式
public static bool IsNull(this object obj)
{
return obj == null;
}
}
}
```
運行結果為:

在注釋中解釋了為什么在空引用中調用擴展方法不會拋出異常的原因,對于這個原因的解釋也不是我個人的猜測的,而是確實如此,其實用IL反匯編程序看看程序生成的中間代碼就可以證明了,下面Main函數中生成的中間代碼即IL(代碼中標注紅色的地方就是s.IsNull()的生成的IL代碼,代碼意思即是調用靜態類NullExten的靜態方法IsNull,此時只是把空引用s傳遞給該方法作為傳入參數,并不是真真在空引用中調用了方法。所以就不存在拋出異常了):
```
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// 代碼大小 43 (0x2b)
.maxstack 2
.locals init ([0] string s)
IL_0000: nop
IL_0001: ldstr bytearray (7A 7A 15 5F 28 75 0A 4E 03 8C 28 75 69 62 55 5C // zz._(u.N..(uibU\
B9 65 D5 6C 14 6F 3A 79 1A FF ) // .e.l.o:y..
IL_0006: call void [mscorlib]System.Console::WriteLine(string)
IL_000b: nop
IL_000c: ldnull
IL_000d: stloc.0
IL_000e: ldstr bytearray (57 5B 26 7B 32 4E 53 00 3A 4E 7A 7A 57 5B 26 7B // W[&{2NS.:NzzW[&{
32 4E 1A FF 7B 00 30 00 7D 00 ) // 2N..{.0.}.
IL_0013: ldloc.0
IL_0014: call **bool ExtensionDefine.NullExten::IsNull(object****)
** IL_0019: box [mscorlib]System.Boolean
IL_001e: call void [mscorlib]System.Console::WriteLine(string,
object)
IL_0023: nop
IL_0024: call valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()
IL_0029: pop
IL_002a: ret
} // end of method Program::Main
```
**四、小結**
到這里本專題的內容就介紹完了,這里總結下該專題介紹的內容:
1. 介紹了擴展方法的定義和使用,以及擴展方法定義的規則,具體可以參照第一部分
2. 介紹了編譯器是如何去發現擴展方法的,以及寫了一些例子進行測試,具體可以參照第二部分
3. 解釋了為什么在空引用中可以調用擴展方法的原因,具體可以參照第三部分
在下一個專題將和大家介紹下C# 3中最重要的一個特性——Linq。
附上:程序中演示源碼:[http://files.cnblogs.com/zhili/%E6%89%A9%E5%B1%95%E6%96%B9%E6%B3%95Demo.zip](http://files.cnblogs.com/zhili/%E6%89%A9%E5%B1%95%E6%96%B9%E6%B3%95Demo.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 領域驅動設計實戰系列總結