一、反射概念: 1、概念: 反射,通俗的講就是我們在只知道一個對象的內部而不瞭解內部結構的情況下,通過反射這個技術可以使我們明確這個對象的內部實現。 在.NET中,反射是重要的機制,它可以動態的分析程式集Assembly,模塊Module,類型Type等等,我們在不需要使用new關鍵的情況下,就可以 ...
一、反射概念:
1、概念:
反射,通俗的講就是我們在只知道一個對象的內部而不瞭解內部結構的情況下,通過反射這個技術可以使我們明確這個對象的內部實現。
在.NET中,反射是重要的機制,它可以動態的分析程式集Assembly,模塊Module,類型Type等等,我們在不需要使用new關鍵的情況下,就可以動態
創建對象,使用對象。降低代碼耦合性提高了程式的靈活性。那麼,反射是怎麼實現的呢?它的內部實現依賴於元數據。元數據,簡單來說,在
公共語言運行時CLR中,是一種二進位信息,用來描述數據,數據的屬性環境等等的一項數據,那麼反射解析數據的內部實現通過元數據實現再
合適不過了。
2、實例:
首先先寫一個你要反射的程式集:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace StudentClass { public class Student { public Student() { } public string Name { get; set; } public int Age { get; set; } public char Gender { get; set; } public string IdCard { get; set; } public string Address { get; set; } private string Mobile { get; set; } public void Eat() { Console.WriteLine("我今天吃啦好多東西"); } public void Sing() { Console.WriteLine("耶耶耶耶耶"); } public int Calculate(int a, int b) { return a + b; } private string PrivateMethod() { return "我是一個私有方法"; } } }
先來看一下程式街、模塊、以及類等信息。
using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; namespace ReflectionInvoke { class Program { static void Main(string[] args) { //獲取程式集信息 Assembly assembly = Assembly.LoadFile(@"E:\測試\StudentClass\StudentClass\bin\Debug\StudentClass.dll"); Console.WriteLine("程式集名字:"+assembly.FullName); Console.WriteLine("程式集位置:"+assembly.Location); Console.WriteLine("運行程式集需要的額CLR版本:"+assembly.ImageRuntimeVersion); Console.WriteLine("===================================================="); //獲取模塊信息 Module[] modules = assembly.GetModules(); foreach (Module item in modules) { Console.WriteLine("模塊名稱:"+item.Name); Console.WriteLine("模塊版本ID"+item.ModuleVersionId); } Console.WriteLine("======================================================"); //獲取類,通過模塊和程式集都可以 Type[] types = assembly.GetTypes(); foreach (Type item in types) { Console.WriteLine("類型的名稱:"+item.Name); Console.WriteLine("類型的完全命名:"+item.FullName); Console.WriteLine("類型的類別:"+item.Attributes); Console.WriteLine("類型的GUID:"+item.GUID); Console.WriteLine("====================================================="); } //獲取主要類Student的成員信息等 Type studentType = assembly.GetType("StudentClass.Student");//完全命名 MemberInfo[] mi = studentType.GetMembers(); foreach (MemberInfo item in mi) { Console.WriteLine("成員的名稱:"+item.Name); Console.WriteLine("成員類別:"+item.MemberType); } Console.WriteLine("====================================="); //獲取方法 BindingFlags flags = BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance; MethodInfo[] methodInfo = studentType.GetMethods(flags); foreach (MethodInfo item in methodInfo) { Console.WriteLine("public類型的,不包括基類繼承的實例方法:"+item.Name); } Console.WriteLine("========================================"); BindingFlags flag = BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.NonPublic; MethodInfo[] methods = studentType.GetMethods(flag); foreach (MethodInfo item in methods) { Console.WriteLine("非public類型的,不包括基類繼承的實例方法:"+item.Name); } Console.WriteLine("========================================"); //獲取屬性 BindingFlags flags2 = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly | BindingFlags.Instance; PropertyInfo[] pi = studentType.GetProperties(flags2); foreach (PropertyInfo item in pi) { Console.WriteLine("屬性名稱:"+item.Name); } } } }
結果:
1、Assembly.Load()以及Assembly.LoadFile():
LoadFile這個方法的參數是程式集的絕對路徑,通過點擊程式集shift+滑鼠右鍵複製路徑即可。load方法有多個重載,還可以通過流的方式獲取程式集,
在項目中,主要用來取相對路徑,因為很多項目的程式集會被生成在一個文件夾里,此時取相對路徑不容易出錯。
2、GetTypes和GetType():
很明顯第一個獲取程式集下所有的類,返回一個數組,第二個要有參數,類名為完全類名:命名空間+類名,用於獲取指定的類。
3、Type類下可以獲取這個類的所有成員,也可以獲取欄位屬性方法等,有:
ConstructorInfo獲取構造函數, FieldInfo獲取欄位, MethodInfo獲取方法,PropertyInfo獲取屬性,EventInfo獲取事件,ParameterInfo獲取參數,通過他們的
Get***獲取,加s獲取所有返回數組,不加s獲取具體的。
4、BindFlags:用於對獲取的成員的類型加以控制:
通過反編譯工具,可以看到這個enum的具體:
BindingFlags.Public公共成員,NonPublic,非公有成員,DeclaredOnly僅僅反射類上聲明的成員不包括簡單繼承的成員。CreateInstance調用構造函數,GetField獲取欄位值對setField無效。還有很多讀者可以F12打開看一下用法以及註釋。註意必須指定:BindingFlags.Instance或BindingFlags.Static,主要為了獲取返回值,是靜態的還是實例的。
二、反射的運用:
1、創建實例:
創建實例大體分為2種,Activator.CreateInstance和Assembly.CreateInstance。這2種方法都可以創建實例,但是又有區別,下麵來通過實例具體說明。
首先分析第一種Activator.CreateInstance
這個方法有許多的重載,最常用的2種:(Type type)和(Type type,params object[] obj)第一種調用無參構造,第二種調用有參構造
在前面的實例Student中添加一個有參構造:
public Student(string name) { this.Name = name; }
然後反射創建實例
Assembly assmbly = Assembly.LoadFile(@"E:\測試\StudentClass\StudentClass\bin\Debug\StudentClass.dll"); Type studentType = assmbly.GetType("StudentClass.Student"); object obj = Activator.CreateInstance(studentType, new object[] { "milktea" }); if (obj != null) { Console.WriteLine(obj.GetType()); }
這裡就創建了一個實例,現在讓我們用反編譯工具查看它的底層實現:
public static object CreateInstance(Type type, params object[] args) { return CreateInstance(type, BindingFlags.CreateInstance | BindingFlags.Public | BindingFlags.Instance, null, args, null, null); }
調用它的參數最多的一個重載後,發現他調用了下麵這個方法:
這裡我們就可以知道這裡創建實例和new創建實例的第三步實現相同,new創建實例,先在堆中開闢新空間,然後創建對象調用它的構造函數,
所以我們可以知道Activator.CreateInstance的底層仍然是通過被調用的類別的構造創建的,那麼如果沒有參數就說明調用的是無參構造。
然後來看第二種Assembly.CreateInstance:
Assembly assmbly = Assembly.LoadFile(@"E:\測試\StudentClass\StudentClass\bin\Debug\StudentClass.dll"); Type studentType = assmbly.GetType("StudentClass.Student"); object o = assmbly.CreateInstance(studentType.FullName,true); Console.WriteLine(o.GetType());
運行程式,卻發現此時拋出了MissingMethodException異常:
可是明明有一個構造函數,為什麼還會說沒有找到構造函數呢?
通過反編譯工具,來看看什麼原因:
我們發現Assembly這個類下的CreateInstance方法,居然返回的是Activator下的CreateInstance方法,那麼就只有一種可能,他調用的
是反射類下的無參構造,而無參構造被我們新加的有參構造給替代了,因此也就找不到無參構造,為了證明結論的正確,我們把無參構造
加上,然後重新實驗:
public Student() { }
果然和我們預想的一樣,如果沒有無參構造,那麼使用Assembly類下的方法就會拋出異常。綜合2種情況,既然Assembly下的CreateInstance
也是調用的Activator的方法,並且Assembly限制更大,那我們在創建實例的時候應當還是選Activator下的方法更不容易出錯,不是嗎。
2、調用方法,屬性賦值等
創建了實例以後,就到了實際用途,怎麼調用它的方法,怎麼給它的欄位賦值,怎麼添加一個委托事件等,現在來看。
A、第一種方法:使用Type類的InvokeMember()方法,實例如下:
Assembly assmbly = Assembly.LoadFile(@"E:\測試\StudentClass\StudentClass\bin\Debug\StudentClass.dll"); Type studentType = assmbly.GetType("StudentClass.Student"); object o = assmbly.CreateInstance(studentType.FullName, true); Type instanceType = o.GetType(); //給屬性賦值並檢查 instanceType.InvokeMember("Name",BindingFlags.SetProperty,null,o,new object[]{"milktea"}); string propertyValue = instanceType.InvokeMember("Name",BindingFlags.GetProperty,null,o,null).ToString(); Console.WriteLine(propertyValue); //調用方法無返回值 instanceType.InvokeMember("Eat",BindingFlags.InvokeMethod,null,o,null); //調用方法有返回值 int sum = Convert.ToInt32(instanceType.InvokeMember("Calculate",BindingFlags.InvokeMethod,null,o,new object[]{2,3})); Console.WriteLine(sum);
幾個重要的參數:第一個方法的名稱,Enum的值,欄位SetField,方法InvokeMethod,然後選擇要使用的對象,即剛纔反射創建的實例,最後一個要
賦的值或者方法參數等必須為一個object數組。
這個方法詳情請看MSDN官方文檔:
B、 第二種方法:使用FiledInfo,MethodInfo...等的Invoke方法,實例如下:
Assembly assmbly = Assembly.LoadFile(@"E:\測試\StudentClass\StudentClass\bin\Debug\StudentClass.dll"); Type studentType = assmbly.GetType("StudentClass.Student"); object o = assmbly.CreateInstance(studentType.FullName, true); Type instanceType = o.GetType(); //給屬性賦值並檢查 PropertyInfo ps = instanceType.GetProperty("Age",typeof(Int32)); ps.SetValue(o,5,null); PropertyInfo pi2 = instanceType.GetProperty("Age"); Console.WriteLine(pi2.GetValue(o,null)); //調用方法 MethodInfo mi = instanceType.GetMethod("Calculate", BindingFlags.Instance|BindingFlags.Public); object obj = mi.Invoke(o, new object[] { 1, 2 }); int result = Convert.ToInt32(mi.Invoke(o,new object[]{1,2})); Console.WriteLine(result);
方法的過程即先通過方法名取的方法,註意參數中的BindingFlags的2個參數都不可以丟,否則會報空引用異常,然後Invoke方法中
第一個參數為反射創建的對象,第二個參數為賦的值,或參數等。
C、第三種方法:對於反射的優化,通過使用委托:這裡我們將使用Stopwatch對比和上次同樣結果的時間:
Assembly assmbly = Assembly.LoadFile(@"E:\測試\StudentClass\StudentClass\bin\Debug\StudentClass.dll"); Type studentType = assmbly.GetType("StudentClass.Student"); object o = assmbly.CreateInstance(studentType.FullName, true); Type instanceType = o.GetType(); //給屬性賦值並檢查 Stopwatch sw = new Stopwatch(); sw.Start(); PropertyInfo ps = instanceType.GetProperty("Age",typeof(int)); ps.SetValue(o,5,null); PropertyInfo pi2 = instanceType.GetProperty("Age"); Console.WriteLine(pi2.GetValue(o,null)); Console.WriteLine("屬性沒啟用優化:"+sw.Elapsed); //調用方法 sw.Reset(); sw.Restart(); MethodInfo mi = instanceType.GetMethod("Calculate", BindingFlags.Instance|BindingFlags.Public); object obj = mi.Invoke(o, new object[] { 1, 2 }); int result = Convert.ToInt32(mi.Invoke(o,new object[]{1,2})); Console.WriteLine(result); Console.WriteLine("方法沒啟用優化:" + sw.Elapsed); //給屬性賦值並檢查 sw.Reset(); sw.Restart(); PropertyInfo pi3 = instanceType.GetProperty("Age", typeof(int)); var piDele = (Action<int>)Delegate.CreateDelegate(typeof(Action<int>),o,pi3.GetSetMethod()); piDele(5); var result1 = (Func<int>)Delegate.CreateDelegate(typeof(Func<int>), o, pi3.GetGetMethod()); Console.WriteLine(result1()); Console.WriteLine("屬性啟用優化:"+sw.Elapsed); //調用方法 sw.Reset(); sw.Restart(); MethodInfo mi2 = instanceType.GetMethod("Calculate",BindingFlags.Instance|BindingFlags.Public); var miDele = (Func<int, int, int>)Delegate.CreateDelegate(typeof(Func<int,int,int>),o,mi2); int a = miDele(1,2); Console.WriteLine(a); Console.WriteLine("方法啟用優化:"+sw.Elapsed);
這裡可以很明顯的看到使用優化以後,時間縮短了斤2/3,試想一下,這裡只用了很少的代碼,如果代碼量很多的話就可以節省更多的時間。
當然也可以看出這裡的代碼量比較大而複雜,可以說不夠漂亮簡介,用空間換取效率,Delegate.CreateDelegate()方法具體請看: 詳情鏈接
D、現在將最後一種,.NET 4.0出現了一個新的關鍵字:dynamic,和var有點類似的感覺,但實則不同。var是語法糖,在代碼編譯期就將真正的類型
已經替換了,Visual Studio可以推斷出var的類型,而dynamic不會在編譯期檢查,被編譯為object類型,而會在運行期做檢查,並且這個效率雖然沒
有優化後的反射快,但比普通的反射也要快一些。
Stopwatch watch1 = Stopwatch.StartNew(); Type type = Assembly.LoadFile(@"E:\C#優化實例\StudentClass\StudentClass\bin\Debug\StudentClass.dll").GetType("StudentClass.Student"); Object o1 = Activator.CreateInstance(type,new object[]{12}); var method1 = type.GetMethod("Add",BindingFlags.Public|BindingFlags.Instance); int num1 = (int)method1.Invoke(o1,new object[]{1,2}); Console.WriteLine(num1); Console.WriteLine("反射耗時"+watch1.ElapsedMilliseconds); Stopwatch watch2 = Stopwatch.StartNew(); Type type2 = Assembly.LoadFile(@"E:\C#優化實例\StudentClass\StudentClass\bin\Debug\StudentClass.dll").GetType("StudentClass.Student"); dynamic o2 = Activator.CreateInstance(type, new object[] { 13 }); int num2 = o2.Add(2,3); Console.WriteLine(num2); Console.WriteLine("dynamic耗時:"+watch2.ElapsedMilliseconds);
這裡看到是比反射要快一些,而且代碼精簡了很多。綜合考慮下來,代碼精簡度以及耗費時間,建議儘量使用dynamic關鍵字來處理反射。
這裡反射的主要點總結完畢,還有不全的方面請評論留言相告,感激感激 2018-11-08 17:29:28