當我們想為一個現有的類型添加一個方法的時候,有兩種方式:一是直接在現有類型中添加方法;但是很多情況下現有類型都是不允許修改的,那么可以使用第二種方式,基於現有類型創建一個子類,然后在子類中添加想要的方法。
當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: nop IL_0020: ldc.i4.s 11 IL_0022: stloc.0 IL_0023: ldstr "Is {0} a even number? {1}" IL_0028: ldloc.0 IL_0029: box [mscorlib]System.Int32 IL_002e: ldloc.0 //直接調用擴展方法 IL_002f: call bool ExtentionMethodTest.ExtentionMethods::IsEven(int32) IL_0034: box [mscorlib]System.Boolean IL_0039: call void [mscorlib]System.Console::WriteLine(string, object, object) IL_003e: nop IL_003f: ldstr "Is {0} a even number? {1}" IL_0044: ldloc.0 IL_0045: box [mscorlib]System.Int32 IL_004a: ldloc.0 //通過靜態類調用靜態方法 IL_004b: call bool ExtentionMethodTest.ExtentionMethods::IsEven(int32) IL_0050: box [mscorlib]System.Boolean IL_0055: call void [mscorlib]System.Console::WriteLine(string, object, object) IL_005a: nop IL_005b: call int32 [mscorlib]System.Console::Read() IL_0060: pop IL_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); } } }
空引用上調用擴展方法
當我們在空引用上調用實例方法是會引發NullReferenceException異常的。
但是,我們可以在空引用上調用擴展方法。
看下面的例子,我們可以判斷一個對象是不是空引用。
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代碼驗證),這也就解釋了為什么空引用上可以調用擴展方法。
總結
本文介紹了擴展方法的使用以及工作原理,其實擴展方法的本質就是通過靜態類調用靜態方法,只不過是編譯器幫我們完成了這個轉換。
然后還介紹了編譯器是如何發現擴展方法的,以及使用擴展方法時要注意的地方。了解了編譯器怎么查找擴展方法,對編寫和調試擴展方法都是有幫助的。