一 前言
反射 这个名词给我的第一印象就是 高深的感觉,虽然项目中有用到,只是通过反射枚举来取值,后来发现居然不需要用反射............
第二个印象就是性能不高,貌似对反射大家已经形成定向思维了,一扯到反射就和性能扯上关系.....
本文抛开性能不谈,因为关于性能,有太多的人做过测试了我就不说什么了,但我相信"存在即合理" 好了 进入正题!
注:本人水平有限,难免会有理解不了或理解错误的地方,还望大家在评论中指出,我会马上更正。谢谢了~
二 反射介绍
2.1 什么是反射
有关程序及其类型的数据被称为元数据,它们保存在程序的程序集中。而程序在运行时,可以查看其他程序集或其本身的元数据。
一个运行的程序查看本身的元数据或其他程序的元数据的行为叫做反射。
2.2 为什么需要反射
2.2.1 当你做一个软件可以安装插件的功能,你连插件的类型名称都不知道,你怎么实例化这个对象呢?因为程序是支持插件的(第三方的),在开发的时候并不知道 。所以,无法在代码中 New出来 ,但反射可以,通过反射,动态加载程序集,然后读出类,检查标记之后再实例化对象,就可以获得正确的类实例。反射的目的就是为了扩展未知的应用。比如你写了一个程序,这个程序定义了一些接口,只要实现了这些接口的dll都可以作为插件来插入到这个程序中。那么怎么实现呢?就可以通过反射来实现。就是把dll加载进内存,然后通过反射的方式来调用dll中的方法。
2.2.2 在编码阶段不知道那个类名,要在运行期从配置文件读取类名, 这时候就没有办法硬编码new ClassName(),而必须用到反射才能创建这个对象.
2.3 与反射相关的类
在了解反射前最好先把与反射相关类之间的关系,以及每个类大致负责哪些操作了解下,有助于对反射有一个结构化的认识,
这张图是我自己在了解时画的,比较原生态,各位就将就下吧,意思到了就行了.....
重点解释下Type类
1 对于程序中用到的每个类型,CLR都会创建 一个包含这个类型信息的Type类型的对象。
2 程序中用到的每个类型都会关联到独立的Type类的对象。
3 不管创建的类型有多少个实例 ,只有一个Type对象会关联到所有这些实例
三 如何使用反射
3.1 加载程序集的方法
以下3.1.1-3.1.3 内容摘抄于 @xiashengwang
3.1.1
Assembly的Load方法
在内部CLR使用Assembly的Load方法来加载这个程序集,这个方法与Win32的LoadLibray等价。在内部,Load导致CLR对程序集应用一个版本重定向策略。并在GAC中查找程序集,如果没有找到,就去应用程序的基目录,私有路径目录和codebase指定的位置查找。如果是一个弱命名程序集,Load不会向程序集应用重定向策略,也不会去GAC中查找程序集。如果找到将返回一个Assembly的引用,如果没有找到则抛出FileNotFoundException异常。注意:Load方法如果已经加载一个相同标识的程序集只会简单的返回这个程序集的引用,而不会去创建一个新的程序集。
大多数动态可扩展应用程序中,Assembly的Load方法是程序集加载到AppDomain的首选方式。这种方式需要指定程序集的标识字符串。对于弱命名程序集只用指定一个名字。
3.1.2
Assembly的LoadFrom方法
当我们知道程序集的路径的场合,可以使用LoadFrom方法,它允许传入一个Path字符串,在内部,LoadFrom首先调用AssemblyName的静态方法GetAssemblyName。这个方法打开指定的文件,通过AssemblyRef元数据表提取程序集的标识,然后关闭文件。随后,LoadFrom在内部调用Assembly的Load方法查找程序集。到这里,他的行为和Load方法是一致的。唯一不同的是,如果按Load的方式没有找到程序集,LoadFrom会加载Path路径指定的程序集。另外,Path可以是URL。如:
Assembly assembly = Assembly.LoadFrom(@"http://www.test.com/LibA.dll");
3.3.3
Assembly的LoadFile方法
这个方法初一看和LoadFrom方法很像。但LoadFile方法不会在内部调用Assembly的Load方法。它只会加载指定Path的程序集,并且这个方法可以从任意路径加载程序集,同一程序集如果在不同的路径下,它允许被多次加载,等于多个同名的程序集加载到了AppDomain中,这一点和上面的两个方法完全不一样。但是,LoadFile并不会加载程序集的依赖项,也就是不会加载程序集引用的其他程序集,这会导致运行时找不到其他参照DLL的异常。要解决这个问题,需要向AppDomain的AssemblyResolve事件登记,在回调方法中显示加载引用的程序集。类似于这样:
AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve); static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) { if (args.Name != null) { return Assembly.LoadFrom(string.Format("{0}\\plugin\\{1}.dll", Application.StartupPath, new AssemblyName(args.Name).Name)); } return null; }
特别注意:要测试LoadFile有没有加载引用的DLL,切不可将DLL拷贝到应用程序的根目录下测试,因为该目录是CLR加载程序集的默认目录,在这个目录中如果存在引用的DLL,它会被加载,造成LoadFile会加载引用DLL的假象。可以在根目录下新建一个子目录如plugin,把引用的dll拷贝到这里面进行测试。
3.2 反射调用同一程序集中的方法
1 internal class PeopleInfo 2 { 3 4 public static string Name = "Zery"; 5 6 public string PrintName(string name) 7 { 8 Console.WriteLine("被反射了,{0}", name); 9 return name; 10 } 11 } 12 13 //通过Type对象直接反射类并创建实例调用类中方法 14 Type type = typeof(PeopleInfo); 15 //创建实例 16 var peopleType = (PeopleInfo)type.Assembly.CreateInstance(type.FullName); 17 //调用方法 18 string name = peopleType.PrintName("Zery"); 19 20 //取字段的值 21 FieldInfo fields = type.GetField("Name"); 22 object value = fields.GetValue(peopleType); 23 24 Console.WriteLine(value);
结果
这种反射好像没什么实际意义,因为可以直接New 一个实例出来,还比反射来得快,但这里还是写下,以证名它是存在的
3.3 反射调用同一工程中不同程序集中方法
1 public class UserInfo 2 { 3 public string name = "I'm Zery"; 4 public int age = 22; 5 public string sex = "Super Man"; 6 7 public string Name 8 { 9 get { return name; } 10 set { name = value; } 11 } 12 13 public int Age 14 { 15 get { return age; } 16 set { age = value; } 17 } 18 public string Sex 19 { 20 get { return sex; } 21 set { sex = value; } 22 } 23 24 public string PrintName(string name) 25 { 26 Console.WriteLine("这是反射调用的Name:{0}", name); 27 return name; 28 } 29 30 } 31 32 33 //一定要在项目中添加引用 34 Assembly assemblyRum = Assembly.Load("PersonInfo"); 35 Type runType = assemblyRum.GetType("PersonInfo.UserInfo"); 36 //创建实例,//Activator: 在本地或从远程创建对象类型,或获取对现有远程对象的引用 37 var obj = Activator.CreateInstance(runType); 38 //获取方法 39 MethodInfo runMethod = runType.GetMethod("PrintName"); 40 //调用方法 41 //注意Invoke方法的两个参数,第一个为类的实例对象如果方法是静态的,则忽略此参数,第二个为方法的参数 42 runMethod.Invoke(obj, new object[] { "Zhang" }); 43 44 //获取字段 45 FieldInfo field = runType.GetField("name"); 46 object value = field.GetValue(obj); 47 Console.WriteLine("获取的字段值:{0}",value);
结果
3.3 反射调用指定路径程序集中的方法
namespace AssembleTest { public class Assemble { public string name = "I'm Zery"; public int age = 22; public string sex = "Super Man"; public string Name { get { return name; } set { name = value; } } public int Age { get { return age; } set { age = value; } } public string Sex { get { return sex; } set { sex = value; } } public string PrintName(string name) { Console.WriteLine("这是反射调用的{0}", name); return name; } public void PrintAge(int age) { Console.WriteLine("这是反射调用的:{0}", age); } public string ReturnValue(int age, string name, string sex) { return string.Format("给反射的返回值:名字:{0},年龄:{1},性别{2}", name, age, sex); } public static void PrintSex(string sex) { Console.WriteLine("这是静态的方法:{0}", sex); } } } //加载指定路径下的程序集 Assembly assembly = Assembly.LoadFile(@"F:\TestFile\AssembleTest.dll"); Assembly formAssembly = Assembly.LoadFrom(@"F:\TestFile\AssembleTest.dll"); Type types = assembly.GetType("AssembleTest.Assemble"); MethodInfo method = types.GetMethod("PrintName"); MethodInfo staticMethod = types.GetMethod("PrintSex"); //反射创建类的实例 Object obj = assembly.CreateInstance("AssembleTest.Assemble"); //需要加名称空间 //反射创建类的实例 //Activator: 在本地或从远程创建对象类型,或获取对现有远程对象的引用 Object acrivatorObj = Activator.CreateInstance(types); //注意Invoke方法的两个参数,第一个为类的实例对象如果方法是静态的,则忽略此参数,第二个为方法的参数 method.Invoke(acrivatorObj, new object[] { "Zery" }); //调用静态方法 第一个参数可以省略 staticMethod.Invoke(null, new object[] { "男" }); //获取字段值 FieldInfo field = types.GetField("name"); var value = field.GetValue(obj); Console.WriteLine("这是字段值:{0}",value);
结果
写了在三种不同情况下通过反射调用程序集中的方法,与字段,反射还有很多功能,如 调用 事件,接口,获取类的属性等等,我写的这里只是反射的冰山一角,若要了解更多,那就得大家自己动手了。
四 总结
虽然反射在性能上有影响,但不能以此为借口,让自己不去了解反射。没写这篇文章之前,看到项目中有一段反射调用其它程序集中方法的代码,只知道结果是调用了方法,却并不知道是如何一步步调用的,而当我写完时,就已经可以将整个调用的流程了解的比较透彻了,所以觉得 写一篇文章,结果并不重要,重要的是写的这个过程会让你收获甚多。
程序员应该走出自己的舒适区,让自己不断的成长。