第一版
内容摘要
前言
关于反射的内容,向来是隐藏在C#高级编程类别中的大部头书籍中,C#入门啊C#精通啊基本少有提及,若是提到也是模棱两可,叫你难以捉摸。 本文本着实用的目的,以《C#与.NET4高级程序设计(第5版)》第四部分,第15章为依据,部分讲述了.NET中反射的用法来分析下反射的实现及简单应用,不涉及原理性能问题的讨论。
在开始反射之前,以下内容需要提前了解,可阅读书中第14章内容:
- 命名空间
- 程序集
- 元数据
反射
反射(Reflection)是一个运行库类型发现的过程。使用反射服务,可以通过编程使用一个友好的对象模型得到元数据信息.
反射提供了描述程序集、模块和类型的对象(Type 类型)。 可以使用反射动态创建类型的实例,将类型绑定到现有对象,或从现有对象获取类型并调用其方法或访问其字段和属性。 如果代码中使用了特性,可以利用反射来访问它们。
反射(C# 和 Visual Basic)
用 .NET Framework 编程时利用反射,可以使用 System.Reflection 命名空间。此命名空间提供封装了很多运行时概念的类,例如程序集、模块、类型、方法、构造函数、字段和属性。
System.Reflection 类语言组件 | 相应的 .NET 类 |
---|---|
程序集 | System.Reflection.Assembly |
模块 | System.Reflection.Module |
抽象成员 | System.Reflection.MemberInfo(以下所有的基类) |
类型 | System.Type |
属性 | System.Reflection.PropertyInfo |
字段 | System.Reflection.FieldInfo |
事件 | System.Reflection.EventInfo |
抽象方法 | System.Reflection.MethodBase(以下所有的基类) |
方法 | System.Reflection.MethodInfo |
构造函数 | System.Reflection.ConstructorInfo |
反射层次结构
- 加载程序集(Assembly)
- 检索元数据(Type、PropertyInfo、FieldInfo、EventInfo、MethodInfo……)
- 动态创建对象/晚期绑定(System.Activator)
- 动态调用方法(Invoke)
查看元数据
元数据查看用到 System.Type
类以及 System.Object.GetType()
、 Type.GetType()
和typeof()
方法
System.Object.GetType()
返回了一个表示当前数据对象的Type类的实例,由示例可以看出,GetType()
需要被一个实例调用,所以,静态类等无法实例化的对象 是无法使用 GetType()
的
1 //只能获取到可以实例化的类2 //静态类无法实例化,所以无法使用此方法获取3 DateTime dt = new DateTime();4 Type type = dt.GetType();
使用Type.GetType()根据类型的完全限定名同样可以获得元数据信息,采用这种方法, 我们不需要从提供该类型的编译时信息,只需要一个类型字符串名称。
//使用类型全名称字符串类获取Type t = Type.GetType("System.Int32");//用来获取静态类元数据var type = Type.GetType("ConsoleApplication1.StaticClass");
使用typeof()
操作符,我们不需要先建立一个实例来提取类型信息,但是,仍然需要知道 类型的编译时信息,因为typeof()
需要的是类型的强类型名称,而不是文本表示。
//使用强类型名称来获取元数据信息var type = typeof(DateTime);//StaticClass 为自定义的静态类type = typeof(StaticClass);
使用上述任一方法都可以获取一个Type类实例,接着可以通过Type类提供的一系列方法类获取到相应的元数据信息,如字段、属性、方法等
Type类下面列举GetMethods()
获取Int类下所有方法的示例,其他内容大致相同,不再罗列
Type t = Type.GetType("System.Int32"); foreach (var method in t.GetMethods()) { Console.WriteLine("->" + method.Name); }
输出
此外,Type类提供了一些其他信息如:
IsAbstract
:是否抽象IsPublic
:是否公共类BaseType
:基类信息- ……
加载程序集
有些场景我们需要在程序运行时以编程的方式载入程序集,即使在引用清单中引用此程序集, 这种按需加载外部程序集的操作被称为动态加载
System.Reflection
定义了一个名为Assembly的类 使用这个类我们可以动态加载程序集,并找到关于程序集自身的属性。使用Assembly类型,我们还可以 动态加载私有或共享程序集,而且能够加载任意位置的程序集。使用Assembly类提供的方法Lode()
和LoadFrom()
来完成上述操作.
Load
和LoadFrom
有众多的重载,所以,可以根据不同的场景使用不同的方法,都返回一个Assembly对象。
public static Assembly Load(AssemblyName assemblyRef); public static Assembly Load(byte[] rawAssembly); public static Assembly Load(string assemblyString); public static Assembly Load(AssemblyName assemblyRef, Evidence assemblySecurity); public static Assembly Load(byte[] rawAssembly, byte[] rawSymbolStore); public static Assembly Load(string assemblyString, Evidence assemblySecurity); public static Assembly Load(byte[] rawAssembly, byte[] rawSymbolStore, Evidence securityEvidence); public static Assembly Load(byte[] rawAssembly, byte[] rawSymbolStore, SecurityContextSource securityContextSource); public static Assembly LoadFile(string path); public static Assembly LoadFile(string path, Evidence securityEvidence); public static Assembly LoadFrom(string assemblyFile); public static Assembly LoadFrom(string assemblyFile, Evidence securityEvidence); public static Assembly LoadFrom(string assemblyFile, byte[] hashValue, AssemblyHashAlgorithm hashAlgorithm); public static Assembly LoadFrom(string assemblyFile, Evidence securityEvidence, byte[] hashValue, AssemblyHashAlgorithm hashAlgorithm);
Assembly.Load
方法仅仅传入一个需要加载到内存的程序集的友好名称,因此,需要把希望反射的文件复制到当前文件Bin(bin\debug)目录下。 Assembly.LoadFrom
方法根据文件的绝对路径来加载指定的文件。还可以指定一个网络路径,如果指定网络路径,则先下载该程序集,然后在加载。
//这种方式需要把文件copy到同目录下bin/debug(Bin)下面 Assembly asm = Assembly.Load("Microsoft.ApplicationBlocks.Data"); Console.WriteLine(asm.FullName); Type[] types = asm.GetTypes(); foreach (var item in types) { Console.WriteLine("Type:{0}", item); }
//DLL文件绝对路径 var assemblyFullPath = @"D:\Lab\Microsoft.ApplicationBlocks.Data.dll"; Assembly asm2 = Assembly.LoadFrom(assemblyFullPath); Console.WriteLine(asm2.FullName); foreach (var item in asm.GetTypes()) { Console.WriteLine("Type:{0}", item.Name); }
晚期绑定
我们已经知道怎么获取加载一个程序集,获取程序集的元数据,那么,我们如何在运行时调用一个未添加在程序引用清单里的类型方法呢?答案是晚期绑定
晚期绑定(late binding)是一种创建一个给定类型的实例并在运行时调用其成员,而不需要在编译时知道它存在的一种技术。当建立一个晚期绑定到外部 程序集类型的应用程序时,因为没有设置该程序集的引用,因此,调用程序清单没有列出这个程序集。
System.Activator
类(定义在mscorlib.dll)是晚期绑定的关键所在。我们只需关注Activator.CreateInstance()
方法,它用来建立一个晚期绑定类型的实例。同样CreateInstance()
方法也是有多次重载,可以根据场景决定使用哪种方式。
static void LateBinding() { var assemblyFullPath = @"D:\Lab\T.Entities.dll"; try { var asm = Assembly.LoadFrom(assemblyFullPath); var types = asm.GetTypes(); var type = asm.GetType("T.Entities.Customers"); object obj = Activator.CreateInstance(type); Console.WriteLine("Create a {0} using Late Binding", obj); } catch (Exception) { throw; } }
方法调用
我们已经加载一个没有添加引用的程序集,并且通过晚期绑定创建了其中某个成员一个实例, 接下来要做的就是要此实例为我们所用,但是,先思考一下,我们在上一个实例中,获取到的是一个object对象, 没有引用就没有办法转换成对应的对象,那我们就无法调用其方法!
解决这个问题我们还是要使用到反射,获取位置对象实例的元数据信息,比如MethodInfo
,通过 MethodInfo.Invoke
来实现方法的调用.
static void InvokeNoParameterMethod() { var assemblyFullPath = @"D:\Lab\T.Entities.dll"; try { var asm = Assembly.LoadFrom(assemblyFullPath); var type = asm.GetType("T.Entities.ModelHelper"); object obj = Activator.CreateInstance(type); Console.WriteLine("Create a {0} using Late Binding", obj); //"GetName"为ModelHelper类中的一个方法,返回一个姓名的字符串 MethodInfo mi = type.GetMethod("GetName"); mi.Invoke(obj, null); string returnStr = mi.Invoke(obj, null) as string; Console.WriteLine(returnStr); } catch (MissingMethodException ex) { throw; } }
反射、晚期绑定和自定义特性的使用背景
原书中提到一个场景 产品必须可以通过使用第三方工具进行扩展,以第三方软件提供商扩展Visual Studio 2010 IDE为例 解决这个问题的一个可能的方法:
- 首先,可扩展的应用程序必须提供一些输入手段,允许用户指定被插入的模块,这需要动态加载
- 其次,为了插入到环境中,可扩展的应用程序必须要确定模块是否支持正确的功能,这需要反射
- 最后,可扩展的应用程序必须获取一个需要的基础架构的引用并调用成员触发底层功能,这经常需要晚期绑定
另外,我遇到的一些情况
- ORM框架经常用到,比如属性自动赋值
- AddIn模式开发
- 抽象工厂模式中,动态生成工厂对象
- ……
实际应用中,并不是所有的场景都需要使用反射,另外普遍观点认为反射会影响到性能,所以没有必要为了显示 你的技术多牛而非要牵扯个反射来。但是作为一种技术手段,了解才会想到运用, 可能会滥用,然后才会逐渐明白其妙处,最后为我所用。