標籤:
C#擴充方法
當我們想為一個現有的類型添加一個方法的時候,有兩種方式:一是直接在現有類型中添加方法;但是很多情況下現有類型都是不允許修改的,那麼可以使用第二種方式,基於現有類型建立一個子類,然後在子類中添加想要的方法。
當C# 2.0中出現了靜態類之後,對於上面的問題,我們也可以建立靜態工具類來實現想要添加的方法。這樣做可以避免建立子類,但是在使用時代碼就沒有那麼直觀了。
其實,上面的方法都不是很好的解決辦法。在C# 3.0中出現了擴充方法,通過擴充方法我們可以直接在一個現有的類型上"添加"方法。當使用擴充方法的時候,可以像調用執行個體方法一樣的方式來調用擴充方法。
擴充方法的使用
擴充方法的建立和使用還是相對比較簡單的。
聲明擴充方法
相比普通方法,擴充方法有它自己的特徵,下面就來看看怎麼聲明一個擴充方法:
- 它必須在一個非嵌套、非泛型的靜態類中(所以擴充方法一定是靜態方法)
- 它至少要有一個參數
- 第一個參數必須加上this關鍵字作為首碼
- 第一個參數類型也稱為擴充類型(extended type),表示該方法對這個類型進行擴充
- 第一個參數不能用其他任何修飾符(比如out或ref)
- 第一個參數的類型不能是指標類型
根據上面的要求,我們給int類型添加了一個擴充方法,用來判斷一個int值是不是偶數:
namespace ExtentionMethodTest{ public static class ExtentionMethods { public static bool IsEven(this int num) { return num % 2 == 0; } } class Program { static void Main(string[] args) { int num = 10; //直接調用擴充方法 Console.WriteLine("Is {0} a even number? {1}", num, num.IsEven()); num = 11; //直接調用擴充方法 Console.WriteLine("Is {0} a even number? {1}", num, num.IsEven()); //通過靜態類調用靜態方法 Console.WriteLine("Is {0} a even number? {1}", num, ExtentionMethods.IsEven(num)); Console.Read(); } }}
雖然這個例子非常簡單,但卻示範了擴充方法的使用。
調用擴充方法
通過上面的例子可以看到,當調用擴充方法的時候,可以像調用執行個體方法一樣。這就是我們使用擴充方法的原因之一,我們可以給一個已有類型"添加"一個方法。
既然擴充方法是一個靜態類的方法,我們當然也可以通過靜態類來調用這個方法。
通過IL可以看到,其實擴充方法也是編譯器為我們做了一些轉換,將擴充方法轉化成靜態類的靜態方法調用
IL_001f: nopIL_0020: ldc.i4.s 11IL_0022: stloc.0IL_0023: ldstr "Is {0} a even number? {1}"IL_0028: ldloc.0IL_0029: box [mscorlib]System.Int32IL_002e: ldloc.0//直接調用擴充方法IL_002f: call bool ExtentionMethodTest.ExtentionMethods::IsEven(int32)IL_0034: box [mscorlib]System.BooleanIL_0039: call void [mscorlib]System.Console::WriteLine(string, object, object)IL_003e: nopIL_003f: ldstr "Is {0} a even number? {1}"IL_0044: ldloc.0IL_0045: box [mscorlib]System.Int32IL_004a: ldloc.0//通過靜態類調用靜態方法IL_004b: call bool ExtentionMethodTest.ExtentionMethods::IsEven(int32)IL_0050: box [mscorlib]System.BooleanIL_0055: call void [mscorlib]System.Console::WriteLine(string, object, object)IL_005a: nopIL_005b: call int32 [mscorlib]System.Console::Read()IL_0060: popIL_0061: ret
有了擴充方法,當調用擴充方法的時候,我們就像是調用一個執行個體方法。但是,我們應該從兩個角度看這個問題:
- 通過擴充方法,可以使一些方法的調用變得更加通俗易懂,與執行個體的關係看起來更協調。就例如,"num.IsEven()"這種寫法。
- 基於這個原因,可以考慮把代碼中靜態工具類中的一些方法變成擴充方法
- 當然正是由於擴充方法的調用跟執行個體方法一樣,所以想要一眼就看出一個方法是不是擴充方法不那麼容易
- 其實在VS中還是很好辨別的,對於上面的例子,在VS中放上滑鼠,就可以看到"(extention) bool int.IsEven()"
擴充方法是怎樣被發現的
知道怎樣調用擴充方法是我們前面部分介紹的,但是知道怎樣不調用擴充方法同樣重要。下面就看看編譯器怎樣決定要使用的擴充方法。
編譯器處理擴充方法的過程:當編譯器看到一個運算式好像是調用一個執行個體方法的時候,編譯器就會尋找所有的執行個體方法,如果沒有找到一個相容的執行個體方法,編譯器就會去尋找一個合適的擴充方法;編譯器會檢查匯入的所有命名空間和當前命名空間中的所有擴充方法,並匹配變數類型到擴充類型存在一個隱式轉換的擴充方法。
當編譯器尋找擴充方法的時候,它會檢查System.Runtime.CompilerServices.ExtensionAttribute屬性來判斷一個方法是否是擴充方法
看到了編譯器怎麼處理擴充方法了,那麼就需要瞭解一下使用擴充方法時要注意的地方了。
擴充方法使用的注意點:
- 執行個體方法的優先順序高於擴充方法
- 當有擴充方法跟執行個體方法簽名一致的時候,編譯器不會給出任何警告,而是預設調用執行個體方法
- 如果存在多個適用的擴充方法,它們可以應用於不同的擴充類型(使用隱式轉換),那麼通過在重載的方法中應用的"更好的轉換"規則,編譯器會選擇最合適的一個
- 在擴充方法的調用中,還有一個規則,編譯器會調用最近的namespace下的擴充方法
下面看一個例子,通過這個例子來更好的理解編譯器處理擴充方法時的一些注意點:
namespace ExtentionMethodTest{ using AnotherNameSpace; public static class ExtentionMethods { public static void printInfo(this Student stu) { Console.WriteLine("printInfo(Student) from ExtentionMethodTest"); Console.WriteLine("{0} is {1} years old", stu.Name, stu.Age); } public static void printInfo(this object stu) { Console.WriteLine("printInfo(object) from ExtentionMethodTest"); Console.WriteLine("{0} is {1} years old", ((Student)stu).Name, ((Student)stu).Age); } } public class Student { public string Name { get; set; } public int Age { get; set; } //執行個體方法 //public void printInfo() //{ // Console.WriteLine("{0} is {1} years old", this.Name, this.Age); //} } class Program { static void Main(string[] args) { Student wilber = new Student { Name = "Wilber", Age = 28 }; //當執行個體方法printInfo存在的時候,所有的擴充方法都不可見 //此時調用的是執行個體方法 //wilber.printInfo(); //當注釋掉執行個體方法後,下面代碼會調用最近的命名空間的printInfo方法 //同時下面語句會選擇“更好的轉換”規則的擴充方法 //printInfo(Student) from ExtentionMethodTest //Wilber is 28 years old wilber.printInfo(); //當把wilber轉換成object類型後,會調用printInfo(this object stu) //printInfo(object) from ExtentionMethodTest //Wilber is 28 years old object will = wilber; will.printInfo(); Console.Read(); } }}namespace AnotherNameSpace{ using ExtentionMethodTest; public static class ExtentionClass { public static void printInfo(this Student stu) { Console.WriteLine("printInfo(Student) from AnotherNameSpace"); Console.WriteLine("{0} is {1} years old", stu.Name, stu.Age); } }}Null 參考上調用擴充方法
當我們在Null 參考上調用執行個體方法是會引發NullReferenceException異常的。
但是,我們可以在Null 參考上調用擴充方法。
看下面的例子,我們可以判斷一個對象是不是Null 參考。
namespace ExtentionMethodTest{ public static class NullUitl { public static bool IsNull(this object o) { return o == null; } } class Program { static void Main(string[] args) { object x = null; Console.WriteLine(x.IsNull()); x = new object(); Console.WriteLine(x.IsNull()); Console.Read(); } }}
通過上面的例子可以看到,即使引用為空白,"x.IsNull()"仍然能夠正常執行。
根據我們前面介紹的擴充方法的工作原理,其實上面的調用會被編譯器轉換為靜態方法的調用"NullUitl.IsNull(x)"(可以查看IL代碼驗證),這也就解釋了為什麼Null 參考上可以調用擴充方法。
總結
本文介紹了擴充方法的使用以及工作原理,其實擴充方法的本質就是通過靜態類調用靜態方法,只不過是編譯器幫我們完成了這個轉換。
然後還介紹了編譯器是如何發現擴充方法的,以及使用擴充方法時要注意的地方。瞭解了編譯器怎麼尋找擴充方法,對編寫和調試擴充方法都是有協助的。
C#擴充方法