先看下面一個動物點名系統的簡單例子:
有一個Animal的抽象動物父類,里面定義了Name、Age兩個屬性和一個Shout()方法,Animal類定義如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Animal { /// <summary> /// 抽象父類 /// </summary> public abstract class Animal { /// <summary> /// Name屬性 /// </summary> public string Name { get; set; } /// <summary> /// Age屬性 /// </summary> public int Age { get; set; } /// <summary> /// Shout抽象方法 /// </summary> public abstract void Shout(); } }
分別定義Cat、Dog類繼承自Animal類,Cat類定義如下:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 namespace Animal 8 { 9 public class Cat :Animal 10 { 11 /// <summary> 12 /// 構造函數初始化 13 /// </summary> 14 public Cat() 15 { 16 base.Name = "湯姆"; 17 base.Age = 2; 18 } 19 20 public override void Shout() 21 { 22 Console.WriteLine("喵喵喵,我是{0},今年{1}歲", 23 base.Name,base.Age); 24 } 25 } 26 }
Dog類定義如下:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 namespace Animal 8 { 9 public class Dog : Animal 10 { 11 /// <summary> 12 /// 構造函數初始化 13 /// </summary> 14 public Dog() 15 { 16 base.Name = "布魯斯"; 17 base.Age = 3; 18 } 19 20 public override void Shout() 21 { 22 Console.WriteLine("汪汪汪,我是{0},今年{1}歲", 23 base.Name, base.Age); 24 } 25 } 26 }
應用場景:在一個控制台程序中,輸入具體的動物的類型,根據輸入的動物類型,輸出Name、Age和Shout()方法,使用傳統方式實現的代碼如下:
1 using Animal; 2 using System; 3 using System.Collections.Generic; 4 using System.Linq; 5 using System.Text; 6 using System.Threading.Tasks; 7 using System.Reflection; 8 9 10 namespace ReflectionCon 11 { 12 class Program 13 { 14 static void Main(string[] args) 15 { 16 Console.WriteLine("請錄入動物類型:"); 17 string type = Console.ReadLine().Trim(); 18 19 Animal.Animal a = null; 20 switch (type) 21 { 22 case "cat": 23 a = new Cat(); 24 a.Shout(); 25 break; 26 case "dog": 27 a = new Cat(); 28 a.Shout(); 29 break; 30 } 31 Console.ReadKey(); 32 } 33 } 34 }
程序運行結果如下:
那么問題來了:如果我們想要增加一個動物類型,那么就需要修改現有的代碼,在switch里面增加判斷。但是這種方式很不利於以后的維護,違反了開閉原則,每次增加一個動物類型的時候,都需要修改代碼。那么有沒有其他方式可以做到不用修改代碼就可以實現呢?答案是肯定的,那就是使用我們接下來要講的反射,先來了解一下什么是反射。
一、什么是反射
在講解什么是反射之前,先來了解應用程序的結構。
程序代碼在編譯后生成可執行的應用,我們首先要了解這種可執行應用程序的結構。
應用程序結果分為應用程序域-程序集-模塊-類型-成員幾個層次,公共語言運行時(CLR)加載器管理應用程序域,這種管理包括將每個程序集加載到相應的應用程序域以及控制每個程序集中類型層次結構的內存布局。
程序集包含模塊,而模塊包含類型,類型又包含成員,反射則提供了封裝程序集、模塊和類型的對象。我們可以使用反射動態地創建類型的實例,將類型綁定到現有對象或從現有對象中獲取類型,然后調用類型的方法或訪問其字段和屬性。
那么究竟什么是反射呢?
反射(Reflection)是.NET中的重要機制,可以在運行時獲得.NET中每一個類型(包括類、結構、委托、接口和枚舉等)的成員,包括方法、屬性、事件、以及構造函數等。還可以獲得每個成員的名稱、限定符和參數等。有了反射,即可對每一個類型了如指掌。如果獲得了構造函數的信息,即可直接創建對象,即使這個對象的類型在編譯時還不知道。
二、反射的用途
1、使用Assembly定義和加載程序集,加載在程序集清單中列出模塊,以及從此程序集中查找類型並創建該類型的實例。
2、使用Module了解包含模塊的程序集以及模塊中的類等,還可以獲取在模塊上定義的所有全局方法或其他特定的非全局方法。
3、使用ConstructorInfo了解構造函數的名稱、參數、訪問修飾符(如public或private)和實現詳細信息(如abstract或virtual)等。使用Type的GetConstructors()或GetConstructor()方法來調用特定的構造函數。
4、使用MethodInfo了解方法的名稱,返回類型、參數、訪問修飾符(如public或private)和實現詳細信息(如abstract或virtual)等。使用Type的GetMethods()或GetMethod()方法來調用特定的方法。
5、使用FiledInfo了解字段的名稱、訪問修飾符(如public或private)和實現詳細信息(如static)等,並獲取或設置字段值。
6、使用EventInfo了解事件的名稱、事件處理程序數據類型、自定義屬性、聲明類型和反射類型等,添加或移除事件處理程序。
7、使用PropertyInfo了解屬性的名稱、數據類型、聲明類型、反射類型和只讀或可寫狀態等,獲取或設置屬性值。
8、使用ParameterInfo了解參數的名稱、數據類型、是輸入參數還是輸出參數,以及參數在方法簽名中的位置等。
三、反射用到的命名空間及主要類
1、命名空間
System.Reflection
System.Type
System.Reflection.Assembly
2、反射用到的主要類
Type類:該類位於System.Type命名空間下面,通過這個類可以訪問任何給定數據類型的信息。
Assembly類:該類位於System.Reflection.Assembly命名空間下面,通過這個類可以訪問給定程序集的信息,或者把這個程序集加載到程序中。
四、Type類
Type類位於System.Type命名空間下面,通過這個類可以訪問關於任何數據類型的信息。
我們以前把Type看作一個類,但它實際上是一個抽象的基類。只要實例化了一個Type對象,實際上就實例化了Type的一個派生類。盡管一般情況下派生類只提供各種Type方法和屬性的不同重載,但是這些方法和屬性返回對應數據類型的正確數據,Type有與每種數據類型對應的派生類。
它們一般不添加新的方法或屬性。通常,獲取指向任何給定類型的Type引用有3種常用方式:
1、使用GetType()方法,所有的類都會從System.Object繼承這個方法
string v = "abc"; Type type = v.GetType();
2、使用Type類的靜態方法GetType()
Type type2 = Type.GetType("System.string", false, true);
3、使用C#的typeof運算符,這個運算符的參數是類型的名稱(但不放在引號中)
var t = typeof(string);
運行結果:
注意:在一個變量上調用GetType()方法,不是把類型的名稱作為其參數。但要注意,返回的Type對象仍只與該數據類型相關。如果引用了一個對象,但不能確保該對象實際上是哪個類型的實例,這個方法就很有用。
4、Type類的屬性
由Type實現的屬性可以分為下述三類:
1)許多屬性都可以獲取包含與類相關的各種名稱的字符串
2)屬性還可以進一步獲取Type對象的引用,這些引用表示相關的類
3)許多Boolean 屬性表示這個類型是一個類、還是一個枚舉等。這些屬性包括IsAbstract、IsArray、IsClass、IsEnum、IsInterface、IsPointer、IsPrimitive(一種預定義的基本數據類型)、 IsPublic、IsSealed和IsValueType
5、Type類的方法
System.Type類的大多數方法都用於獲取對應數據類型的成員信息:構造函數、屬性、方法和事件等。它有許多方法,但它們都有相同的模式。
例如,有兩個方法可以獲取數據類型的方法信息:GetMethod() 和 GetMethods()。GetMethod()方法返回System.Reflection.MethodInfo對象的一個引用,其中包含一個方法的信息。GetMethods()返回這種引用的一個數組。其區別是GetMethods()返回所有方法的信息,而GetMethod()返回一個方法的信息,其中該方法包含特定的參數列表。這兩個方法都有重載方法,該重載方法有一個附加的參數,BindingFlags枚舉值,表示應返回哪些成員,例如,返回公有成員、實例成員和靜態成員等。
Type的成員方法:
注意:GetMember() 和 GetMembers()方法返回數據類型的一個或所有成員的信息,這些成員可以是構造函數、屬性和方法等。最后要注意,可以調用這些成員,其方式是調用Type的InvokeMember()方法,或者調用MethodInfo, PropertyInfo和其他類的Invoke()方法。
五、Assembly類
Assembly類在System.Reflection名稱空間中定義,它允許訪問給定程序集的元數據,它也包含可以加載和執行程序集(假定該程序集是可執行的)的方法。與Type類一樣,Assembly類包含非常多的方法和屬性。
在使用Assembly實例做一些工作前,需要把相應的程序集加載到正在運行的進程中。為此,可以使用靜態成員Assembly.Load()或Assembly.LoadFrom()這兩個方法的區別是:
Load()方法的參數是程序集的名稱,運行庫會在各個位置上搜索該程序集,試圖找到該程序集,這些位置包括本地目錄和全局程序集緩存。使用Load()方法前要添加程序集的引用。
LoadFrom()方法的參數是程序集的完整路徑名,它不會在其他位置搜索該程程序集。
例如:
Assembly assembly1 = Assembly.Load("Animal"); Assembly assembly1 = Assembly.LoadFrom(@"D:\Study\Practice\Animal.dll");
這兩個方法都有許多其他重載版本,它們提供了其他安全信息。加載了一個程序集后,就可以使用它的各種屬性進行查詢,例如,查找它的全名:
string name = assembly1.FullName;
Assembly類的一個功能是它可以獲得在相應程序集中定義的所有類型的詳細信息,只要調用Assembly以GetTypes()方法,它就可以返回一個包含所有類型的詳細信息的Type類型的引用數組:
Type[] types = assembly.GetTypes(); foreach (Type definedType in types) ( //處理代碼 )
六、使用反射實現上面的程序
經過上面的講解,相信大家對反射有一定的了解了,下面將會使用反射實現開篇提到的應用場景,代碼如下:
1 using Animal; 2 using System; 3 using System.Collections.Generic; 4 using System.Linq; 5 using System.Text; 6 using System.Threading.Tasks; 7 using System.Reflection; 8 9 10 namespace ReflectionCon 11 { 12 class Program 13 { 14 static void Main(string[] args) 15 { 16 Console.WriteLine("請錄入動物類型:"); 17 string type = Console.ReadLine().Trim(); 18 19 // 創建程序集對象,靜態加載Animal程序集 前提:需要先添加對Animal程序集的引用 20 Assembly assembly = Assembly.Load("Animal"); 21 // 獲取程序集中的類型(在這里指的就是Animal里面的類:即Cat、Dog、Pig、Bird類) 22 Type[] types = assembly.GetTypes(); 23 foreach (Type t in types) 24 { 25 // t.Name表示類名(即Cat、Dog、Pig、Bird) 26 if (type == t.Name.ToLower()) 27 { 28 // 找到Shout方法 29 MethodInfo m = t.GetMethod("Shout"); 30 // 創建對象 31 object o = Activator.CreateInstance(t); 32 33 // 找屬性 34 PropertyInfo[] para = t.GetProperties(); 35 // 遍歷屬性 36 foreach (PropertyInfo p in para) 37 { 38 // 輸出屬性的名字 即:Name和Age 39 //Console.WriteLine(p.Name); 40 if (p.Name == "Name") 41 { 42 // 給屬性賦值 43 p.SetValue(o, "張三", null); 44 } 45 if (p.Name == "Age") 46 { 47 // 獲取o對象的屬性為p的屬性值並加10 48 int age = Convert.ToInt32(p.GetValue(o)) + 10; 49 // 給屬性賦值 50 p.SetValue(o, age, null); 51 } 52 } 53 54 // 調用方法 55 m.Invoke(o, null); 56 } 57 } 58 59 Console.ReadKey(); 60 } 61 } 62 }
運行程序:
如果新增加一個動物類,只需要實現Animal抽象父類即可,而主程序不需要修改。
七、反射的優缺點
1、反射的優點
1)、反射提高了程序的靈活性和擴展性。
2)、降低耦合性,提高自適應能力。
3)、它允許程序動態創建和控制任何類的對象,無需提前硬編碼目標類。適用在程序集不固定的地方,通常和配置文件一起使用。
2、反射的缺點
1)、性能問題:使用反射基本上是一種解釋操作,用於字段和方法接入時要遠慢於直接代碼。因此反射機制主要應用在對靈活性和拓展性要求很高的系統框架上,普通程序不建議使用。
2)、使用反射會模糊程序內部邏輯;程序員希望在源代碼中看到程序的邏輯,反射卻繞過了源代碼的技術,因而會帶來維護的問題,反射代碼比相應的直接代碼更復雜。
代碼下載地址:https://pan.baidu.com/s/1nuCx61V