C# 3引入了擴展方法的概念,它既有靜態方法的優點,又使調用它們的代碼的可讀性得到了提高。使用擴展方法,可以像調用實例方法那樣調用靜態方法。
一、擴展方法的語法
1、聲明擴展方法
並不是任何方法都能作爲擴展方法使用——它必須具有以下特徵:
- 它必須在一個非嵌套的、非泛型的靜態類中(所以必須是一個靜態方法);
- 它至少要有一個參數;
- 第一個參數必須附加this關鍵字作爲前綴;
- 第一個參數不能有其他任何修飾符(比如out或ref);
- 第一個參數的類型不能是指針類型。
我們將第一個參數的類型稱爲方法的擴展類型(extended type),即指該方法擴展了該類型。
2、調用擴展方法
編譯器已將擴展方法調用轉換成對普通靜態方法的調用。調用時,會將調用方的值作爲第一個實參的值傳遞。
3、擴展方法是怎樣被發現的
①如果使用using指令,擴展方法可以像類一樣不加限制地在代碼中使用。如果編譯器認爲一個表達式好像是要使用一個實例方法,但沒有找到與這個方法調用兼容的實例方法,就會查找一個合適的擴展方法。它會檢查導入的所有命名空間和當前命名空間中的所有擴展方法,並匹配那些從表達式類型到擴展類型存在着隱式轉換的擴展方法。
②爲了決定是否使用一個擴展方法,編譯器必須能區分擴展方法與某靜態類中恰好具有合適簽名的其他方法。爲此,它會檢查類和方法是否具有System.Runtime.CompilerServices.ExtensionAttribute這個特性(它是.NET 3.5新增的)。但是,編譯器不檢查特性來自哪個程序集。這意味着即使你的項目面向的是.NET 2.0,仍然可以使用擴展方法——只需在正確的命名空間中使用正確的名稱來定義自己的屬性就可以了。然後,你可以聲明擴展方法,該特性會自動應用到方法和類上。編譯器還會將該特性應用到包含擴展方法的程序集上。
③如果存在多個適用的擴展方法,它們可應用於不同的擴展類型(使用隱式轉換),那麼將使用在重載的方法中應用的“更好的轉換”規則,來選擇最合適的方法。要注意的一個重點是,如果存在適當的實例方法,則實例方法肯定會先於擴展方法使用。但是,編譯器不會警告你存在一個和現有的實例方法匹配的擴展方法。
④擴展方法應用於代碼的方式還存在一個潛在的問題——它的應用範圍過於寬泛。如果同一個命名空間中的兩個類含有擴展類型相同的方法,就沒辦法做到只用其中一個類中的擴展方法。
4、空引用上調用方法
在C#中,你不能在空引用上調用實例方法,但你可以在空引用上調用擴展方法。在C# 3中,擴展方法可以和擴展類型的一個現有的靜態方法具有相同的簽名
二、.NET 3.5中的擴展方法
擴展方法最大的用途就是爲LINQ服務。Enumerable和Queryable是兩個類特別醒目的的擴展方法,兩者都在System.Linq命名空間中。在這兩個類中,含有許許多多的擴展方法:Enumerable的大多數擴展的是IEnumerable
1、從Enumerable開始起步
在Enumerable中,有幾個方法不是擴展方法,如Range方法,它獲取兩個int參數:一個起始數,一個是要生成的結果的數目。結果是一個IEnumerable
框架提供的擴展方法會盡量嘗試對數據進行“流式”(stream)或者說“管道”(pipe)傳輸。要求一個迭代器提供下一個元素時,它通常會從它鏈接的迭代器獲取一個元素,處理那個元素,再返回符合要求的結果,而不用佔用自己更多的存儲空間。執行簡單的轉換和過濾操作時,這樣做非常簡單,可用的數據處理起來也非常高效。但是,對於某些操作來說,比如反轉或排序,就要求所有數據都處於可用狀態,所以需要加載所有數據到內存來執行批處理。緩衝和管道傳輸方式,這兩者的差別很像是加載整個DataSet讀取數據和用一個DataReader來每次處理一條記錄的差別。使用LINQ時務必想好真正需要的是什麼,一個簡單的方法調用可能會嚴重影響性能。
2、用Where過濾並將方法調用鏈接到一起
Where擴展方法是對集合進行過濾的一種簡單但又十分強大的方式:它接受一個謂詞,並將其應用於原始集合中的每個元素。Where同樣返回一個IEnumerable
3、用Select方法和匿名類型進行投影
Enumerable中最重要的投影方法就是Select——它操縱一個IEnumerable
4、用OrderBy方法進行排序
LINQ操作符是無副作用的:它們不會影響輸入,也不會改變環境。因而OrderBy排序不會改變原有集合——它返回的是新的序列,所產生的數據與輸入序列相同,當然除了順序。這與List
三、使用思路和原則
1、擴展世界”和使接口更豐富
對於一個給定的問題,程序員通常習慣於構建一個解決方案,直到最終能滿足需求。現在,我們可以擴展世界來迎合解決方案,而不是一直構建方案,直到最終滿足需求。如果一個庫沒有提供你需要的,就擴展這個庫來滿足你的需求。
2、流暢接口
在框架中,流暢接口的一個很好的例子就是OrderBy和ThenBy方法:用Lambda表達式稍加詮釋,代碼準確地描述了它要做的事情。這樣的語句能像完整的英文句子那樣讀,而不是由獨立的“名詞動詞化”的短語構成。
3、理智使用擴展方法
- 將擴展方法放到它們自己的命名空間,可有效防止被誤用。
- 在擴展廣泛使用的類型(如數字、object等)之前,或編寫擴展類型實際爲類型參數這樣的擴展方法之前,要深思熟慮。
- 寫擴展方法應該始終是一個有意識的決定,不要把它培養成一個習慣。絕對不是每個靜態方法都該變成一個擴展方法。
- 在文檔中指出第一個參數(在該值上調用擴展方法)是否允許爲null——如果不允許,就在方法中檢查值,並在必要的時候拋出一個異常(Argument Null Exception)。
- 如果方法名已經在擴展類型中使用,就不要再使用這個名稱。如果擴展類型是框架中的類型,或者來自某個第三方庫,請在庫的版本發生改變時檢查自己的所有擴展方法。
- 將應用於同一個擴展類型的擴展方法分組到一個靜態類中。有的時候,相關的類(比如DateTime和TimeSpan)的擴展方法可以分組到一起。但是,如果擴展方法作用於迥然不同的類型(比如Stream和string),就不要把它們分組到同一個類中了。
- 在兩個不同的命名空間中添加名字相同、擴展類型也相同的兩個擴展方法時一定要三思,尤其是在兩個方法都適用(它們有相同數量的參數)的情況下。合理的做法是添加或刪除一個using指令,就可以使程序構建失敗。但是,即使添加或刪除一個using指令,程序也能構建,只是行爲可能已經發生了變化①,這樣就比較煩人了。