程序集加载和反射
前言
本篇讨论程序集的加载及反射。主要涉及到System.Reflection.Assembly和System.Type两个类,前者可以用于访问指定程序集的相关信息,或把程序集加载到程序当中,后者可以访问任何数据类型的信息。以下,是本篇文章涉及的主要内容。
##程序集加载
本节首先介绍Assembly类,该类位于System.Reflection命名空间下,它允许访问指定程序集的元数据,也包含加载和执行程序集的中的方法。下面将介绍几种常用的动态加载程序集的方式:
方法名称 | 说明 |
---|---|
Load | 加载程序集 |
LoadFrom | 加载指定路径的程序集 |
LoadFile | 仅加载指定路径的程序集(不包括依赖项) |
ReflectionOnlyLoad | 加载程序集(不执行任何带代码) |
ReflectionOnlyLoadFrom | 加载指定路径的程序集(不执行任何代码) |
Assembly.Load
Assembly的Load方法有几个重载版本,两种最常用的重载:Load(AssemblyName)和Load(String),传入的参数是需要加载的程序集的名称。
创建控制台应用程序AssemblyAndReflection,向解决方案添加新项目AssemblyLoad,添加类ClassA、ClassB、ClassC,编译后为AssemblyLoad.dll分配强名称并注册到GAC中。
public class ClassA
{
public void SayHello()
{
Console.WriteLine("Hello.This is ClassA");
}
}
public class ClassB
{
public void SayHello()
{
Console.WriteLine("Hello.This is ClassB");
}
}
public class ClassC
{
public void SayHello()
{
Console.WriteLine("Hello.This is ClassC");
}
}
向Program.cs中添加如下代码:
static void Main(string[] args)
{
string fullName = "AssemblyLoad,Version=1.0.0.0,Culture=neutral,PublicKeyToken=098608575f7409cd, processor architecture=MSIL";
//string fullName = "AssemblyLoad";
//Assembly assembly = Assembly.Load(new AssemblyName(fullName));
Assembly assembly = Assembly.Load(fullName);
if (assembly != null)
{
foreach (var c in assembly.GetTypes())
{
Console.WriteLine(c.FullName);
}
}
Console.ReadLine();
//****************************************************OutPut****************************************************
//AssemblyLoad.ClassA
//AssemblyLoad.ClassB
//AssemblyLoad.ClassC
//**************************************************************************************************************
}
注意,Load方法的参数可以是强命名程序集或弱命名程序集(上述代码中注释掉的fullName变量)。传入不同参数时,查找程序集的方式略有不同。
Assembly.LoadFrom
Assembly的LoadFrom方法加载指定了路径名的程序集。将AssemblyLoad.dll文件放至D:\DLL\下,修改上述代码:
static void Main(string[] args)
{
Assembly assembly = Assembly.LoadFrom(@"D:\DLL\AssemblyLoad.dll");
if (assembly != null)
{
foreach (var c in assembly.GetTypes())
{
Console.WriteLine(c.FullName);
}
}
Console.ReadLine();
//****************************************************OutPut****************************************************
//AssemblyLoad.ClassA
//AssemblyLoad.ClassB
//AssemblyLoad.ClassC
//**************************************************************************************************************
}
LoadFrom的执行原理:
-
调用System.Reflection.AssemblyName类的静态方法GetAssemblyName方法,打开指定路径下的文件,返回AssemblyName对象。下面是GetAssemblyName方法的源码:
[System.Security.SecuritySafeCritical] // auto-generated [ResourceExposure(ResourceScope.None)] [ResourceConsumption(ResourceScope.Machine, ResourceScope.Machine)] static public AssemblyName GetAssemblyName(String assemblyFile) { if(assemblyFile == null) throw new ArgumentNullException("assemblyFile"); Contract.EndContractBlock(); // Assembly.GetNameInternal() will not demand path discovery // permission, so do that first. String fullPath = Path.GetFullPathInternal(assemblyFile); new FileIOPermission( FileIOPermissionAccess.PathDiscovery, fullPath ).Demand(); return nGetFileInformation(fullPath); }
-
调用Assembly.Load方法,将步骤1中返回的AssemblyName对象作为参数传入。
Assembly.LoadFile
加载指定路径上的程序集文件的内容。LoadFile方法不会加载目标程序集引用和依赖的其他程序集。
Assembly.ReflectionOnlyLoad和Assembly.ReflectionOnlyLoadFrom
如果只希望通过反射来分析程序集的元数据,并确保程序集中的任何代码都不会被执行,这种情况下可以使用Assembly类的ReflectionOnlyLoadFrom方法或ReflectionOnlyLoad方法。
##获取类型的信息
本节介绍System.Type类,通过这个类可以访问任何数据类型的信息,System.Type类型是执行类型和对象操作的起点。获取Type对象的几种方式:
Object.GetType()
int x = 100;
Type t = x.GetType();
Console.WriteLine(t.FullName);
System.Type类提供的静态方法ReflectionOnlyGetType()
string typeName = Type.ReflectionOnlyGetType("AssemblyLoad.ClassA, AssemblyLoad, Version=1.0.0.0, Culture=neutral, PublicKeyToken=098608575f7409cd, processor architecture=MSIL", false, true).FullName;
Console.WriteLine(typeName);
System.Reflection.Assembly类提供的实例成员GetTypes、DefinedTypes和ExportedTypes
string fullName = "AssemblyLoad,Version=1.0.0.0,Culture=neutral,PublicKeyToken=098608575f7409cd, processor architecture=MSIL";
Assembly assembly = Assembly.Load(fullName);
Console.WriteLine("assembly.GetTypes():");
foreach (var t in assembly.GetTypes())
{
Console.WriteLine(t.FullName);
}
Console.WriteLine();
Console.WriteLine("assembly.ExportedTypes:");
foreach (var t in assembly.ExportedTypes)
{
Console.WriteLine(t.FullName);
}
Console.WriteLine();
Console.WriteLine("assembly.DefinedTypes:");
foreach (var t in assembly.DefinedTypes)
{
Console.WriteLine(t.FullName);
}
Console.WriteLine();
typeof关键字(应尽量使用这个操作符来获取Type引用,因为操作符生成的代码通常更快)
Console.WriteLine(typeof(int).FullName);
##构造类型的实例
获取对Type派生对象的引用之后,就可以构造该类型的实例了。
System.Activator.CreateInstance
string fullName = "AssemblyLoad,Version=1.0.0.0,Culture=neutral,PublicKeyToken=098608575f7409cd, processor architecture=MSIL";
Type t = Assembly.Load(fullName).GetType("AssemblyLoad.ClassA");
var o = Activator.CreateInstance(t);
Console.WriteLine(o.GetType());
##设计支持加载项的应用程序
反射的性能
反射是相当强大的机制,允许在运行时发现并使用编译时还不太了解的类型及成员。但是,反射也存在如下缺点:
- 反射造成编译时无法保证类型安全
- 反射速度慢。
基于上述原因,应尽量避免使用反射来访问字段或调用方法及属性。在设计支持加载项的应用程序时,让类型实现编译时已知的接口,在运行时构造类型的实例,将对它的引用放到接口类型的变量中,再调用接口定义的方法。
创建支持加载项的应用程序
-
添加CoreLib项目,创建ISayHello接口并为接口定义SayHello方法
namespace ISayHello { public interface ISayHello { void SayHello(); } }
-
修改AssemblyLoad,添加CoreLib引用,并使其中的ClassA、ClassB、ClassC分别实现接口ISayHello,重新编译,将AssemblyLoad.dll文件拷贝至D:\DLL\AssemblyLoad.dll
using CoreLib; namespace AssemblyLoad { public class ClassA : ISayHello { public void SayHello() { Console.WriteLine("Hello.This is ClassA"); } } public class ClassB : ISayHello { public void SayHello() { Console.WriteLine("Hello.This is ClassB"); } } public class ClassC : ISayHello { public void SayHello() { Console.WriteLine("Hello.This is ClassC"); } } }
-
回到控制台应用程序AssemblyAndReflection,添加CoreLib引用,向App.config中添加配置节:
<appSettings>
<add key="Test" value="ClassA"/>
</appSettings>修改Program中的Main方法:
class Program { static void Main(string[] args) { string type = ConfigurationManager.AppSettings["Test"]; Assembly assembly = Assembly.LoadFrom(@"D:\DLL\AssemblyLoad.dll"); var q = from r in assembly.ExportedTypes where r.IsClass && typeof(ISayHello).GetTypeInfo().IsAssignableFrom(r.GetTypeInfo()) && r.Name == type select r; foreach (var t in q) { ISayHello s = (ISayHello)Activator.CreateInstance(t); s.SayHello(); } Console.ReadLine(); } }
启动程序,控制台输出Hello.This is ClassA,以上示例完成了根据配置文件中的参数调用指定类型下的方法,当然,也可以把参数放在数据库中。
项目结构示意图:
使用反射获取类型的成员
获取类型的成员
抽象基类System.Reflection.MemberInfo封装了所有类型成员都通用的一组属性。MemberInfo有许多派生类,每个都封装了与特性类型成员相关的更多属性,以下是这些类型的层次结构图:
调用类型的成员:
成员类型 | 调用(Invoke)成员需要调用的方法 |
---|---|
FieldInfo | 调用GetValue获取字段的值 |
调用SetValue设置字段的值 | |
ConstructorInfo | 调用Invoke构造类型的实例并调用构造器 |
MethodInfo | 调用Invoke来调用类型的方法 |
PropertyInfo | 调用GetValue获取属性的get访问器方法 |
调用SetValue获取属性的set访问器方法 | |
EventInfo | 调用AddEventHandler来调用事件的add访问器方法 |
调用RemoveEventHandler来调用时间的remove访问器方法 |
使用绑定句柄减少进程的内存消耗
Type和MemberInfo类型的对象需要大量的内存,如果将这些对象保存在集合当中,可能对程序的性能产生负面的影响。如果需要保存/缓存大量的Type和MemberInfo对象,可以使用运行时句柄代替对象以减少占用的内存。System命名空间下有三个运行时句柄类型:
- RuntimeTypeHandle
- RuntimeFieldHandle
- RuntimeMethodHandle
以下是《CLR Via C#》第4版中的示例(博主已经想不到更贴切的示例了):
class Program
{
private const BindingFlags c_bf = BindingFlags.FlattenHierarchy |
BindingFlags.Instance | BindingFlags.Static |
BindingFlags.Public | BindingFlags.NonPublic;
static void Main(string[] args)
{
//显示在任何反射操作之前堆的大小
Show("Before doing anything");
//为MScorlib.dll中的所有方法构建MethodInfo对象缓存
List<MethodBase> methodInfos = new List<MethodBase>();
foreach (Type t in typeof(Object).Assembly.GetExportedTypes())
{
//跳过所有泛型类型
if (t.IsGenericTypeDefinition)
{
continue;
}
MethodBase[] mb = t.GetMethods(c_bf);
methodInfos.AddRange(mb);
}
//显示当绑定所有方法之后,方法的个数和堆的大小
Console.WriteLine("# of methods={0:N0}", methodInfos.Count);
Show("After building cache of MethodInfo obejcts");
//为所有MethodInfo对象构建RuntimeMethodHandle缓存
List<RuntimeMethodHandle> methodHandles = methodInfos.ConvertAll<RuntimeMethodHandle>(m => m.MethodHandle);
Show("Holding MethodInfo and RuntimeMethodHandle cache");
GC.KeepAlive(methodInfos); //组织缓存被过早垃圾回收
methodInfos = null; //现在允许缓存垃圾回收
Show("After freeing MethodInfo objects");
methodInfos = methodHandles.ConvertAll<MethodBase>(rmh => MethodBase.GetMethodFromHandle(rmh));
Show("Size of heap after re-creating MethodInfo objects");
GC.KeepAlive(methodHandles);
GC.KeepAlive(methodInfos);
methodHandles = null;
methodInfos = null;
Show("After freeing MethodInfos and RuntimeMethodHandles");
Console.ReadLine();
}
private static void Show(string s)
{
Console.WriteLine("Heap size={0,12:N0} - {1}", GC.GetTotalMemory(true), s);
}
}