先看下面一个动物点名系统的简单例子:
有一个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