今天群里一個小伙伴問了這樣一個問題,擴展方法與實例方法的執行順序是什么樣子的,誰先誰后(這個問題會在文章結尾回答)。所以寫了這邊文章,力圖從原理角度解釋擴展方法及其使用。
以下為主要內容:
-
什么是擴展方法
-
擴展方法原理及自定義擴展方法
-
擴展方法的使用及其注意事項
一般而言,擴展方法為現有類型添加新的方法(從面向對象的角度來說,是為現有對象添加新的行為)而無需修改原有類型,這是一種無侵入而且非常安全的方式。擴展方法是靜態的,它的使用和其他實例方法幾乎沒有什么區別。常見的擴展方法有Linq擴展、有IEnumerable擴展等。
先讓我們來感受一下.NET中自帶的擴展方法,其中OrderBy和Aggregate都是系統自帶的擴展方法
using System; using System.Collections.Generic; using System.Linq; namespace Test { class Program { static void Main(string[] args) { List<int> lst = new List<int> { 2, 1, 4, 3 }; string result = lst.OrderBy(p => p).Aggregate(string.Empty, (next, p) => next += p + ","); Console.WriteLine(result); Console.ReadLine(); } } }
輸出結果:
是不是感覺擴展方法很優美,使用起來和實例方法幾乎沒有區別。不得不說.NET在這方面做得很精致,很讓人欽佩,那么接下來我們來看看擴展方法的原理
首先我們,先看看如何自定義擴展方法
using System; using TestExtension; namespace Test { class Program { static void Main(string[] args) { Console.WriteLine("2".ToInt32()); Console.ReadLine(); } } } namespace TestExtension { public static class StringExtension { public static int ToInt32(this string str) { if (int.TryParse(str, out int result)) { return result; } throw new ArgumentException("無法轉換為Int32類型"); } } }
通過以上實例,我們可以知道自定義擴展方法需要做到:
-
必須是靜態類,擴展方法也為靜態方法
-
此方法的第一個參數指定方法所操作的類型;此參數前面必須加上 this 修飾符
-
在調用代碼中,如何不再同一個命名空間,需要添加 using 指令,導入需要調用的擴展方法所在的命名空間
-
需要注意的是,第一個this標記的參數並非是實參,而是標識該擴展所指定的類型,調用的時候只需要提供this后的形參即可
接下來我們來探究一下擴展方法反編譯后的效果:
這是StringExtension編譯后的代碼,可以看到擴展方法在編譯后被標記了ExtensionAttribute這個特性,也就是說擴展方法在編譯期就已經被綁定成擴展方法了
.class public auto ansi abstract sealed beforefieldinit TestExtension.StringExtension extends [System.Runtime]System.Object { .custom instance void[System.Runtime]System.Runtime.CompilerServices.ExtensionAttribute
::.ctor() = ( 01 00 00 00 ) // Methods .method public hidebysig static int32 ToInt32 ( string str ) cil managed { .custom instance void[System.Runtime]System.Runtime.CompilerServices.ExtensionAttribute
::.ctor() = ( 01 00 00 00 ) // Method begins at RVA 0x2050 // Code size 31 (0x1f) .maxstack 2 .locals init ( [0] int32, [1] bool, [2] int32 ) IL_0000: nop IL_0001: ldarg.0 IL_0002: ldloca.s 0 IL_0004: call bool [System.Runtime]System.Int32::TryParse(string, int32&) IL_0009: stloc.1 IL_000a: ldloc.1 IL_000b: brfalse.s IL_0012 IL_000d: nop IL_000e: ldloc.0 IL_000f: stloc.2 IL_0010: br.s IL_001d IL_0012: ldstr "無法轉換為Int32類型" IL_0017: newobj instance void [System.Runtime]System.ArgumentException::.ctor(string) IL_001c: throw IL_001d: ldloc.2 IL_001e: ret } // end of method StringExtension::ToInt32 } // end of class TestExtension.StringExtension
我們看一下調用后的效果,和直接調用靜態方法一樣TestExtension.StringExtension::ToInt32(string) ,至此,我們已經知道了擴展方法的使用了,編譯器綁定,底層調用和靜態調用一直,這也解釋了一個問題,就是當類型為空的時候,為什么調用擴展方法了
.namespace Test { .class private auto ansi beforefieldinit Test.Program extends [System.Runtime]System.Object { // Methods .method private hidebysig static void Main ( string[] args ) cil managed { // Method begins at RVA 0x207b // Code size 24 (0x18) .maxstack 8 .entrypoint IL_0000: nop IL_0001: ldstr "2" IL_0006: call int32 TestExtension.StringExtension::ToInt32(string) IL_000b: call void [System.Console]System.Console::WriteLine(int32) IL_0010: nop IL_0011: call string [System.Console]System.Console::ReadLine() IL_0016: pop IL_0017: ret } // end of method Program::Main .method public hidebysig specialname rtspecialname instance void .ctor () cil managed { // Method begins at RVA 0x2094 // Code size 8 (0x8) .maxstack 8 IL_0000: ldarg.0 IL_0001: call instance void [System.Runtime]System.Object::.ctor() IL_0006: nop IL_0007: ret } // end of method Program::.ctor } // end of class Test.Program }
擴展方法雖然很好用,但是如果我們擴展的對象發生了版本迭代,則會增加擴展方法失效的風險。
一下是在使用擴展方法時需要注意的地方
-
擴展方法與該類型中定義的方法具有相同的簽名,編譯器總是綁定到該實例方法,也就是擴展方法永遠不會被調用,這也就回答了題目剛開始所說的問題。同時這個地方應該是考慮到了程序安全的問題,不然很容易出現代碼注入問題。
-
當出現命名空間不同時,則需要使用
using導入命名空間
-
同時擴展方法可以被修飾為internal,public,但需要類和擴展方法保持同樣的修飾標識