C#动态程序集的加载、创建实例、序列化与反序列化
http://hi.baidu.com/linrao/blog/item/6086dc3918df18d5d4622534.html
什么是动态程序集
动态程序集是指没有被编译至主程序,而是主程序在运行时动态调用或者生成的程序集。动态链接库(dll)大家一定很熟悉,它是一系列类和方法的打包,可以被不同的执行程序加载和共享,而C#生成的程序集也是dll。在实际应用中,一些程序不一定要在启动的时候就把所有dll文件都加载到内存中,而是根据需要动态加载或者创建。C#的反射和晚期绑定为实现这种操作提供了极大的帮助。本文主要介绍了从动态程序集里怎样创建特定类的对象,以及该对象的序列化和反序列化问题。
C#的加载程序集的机制
我们知道,一个C#应用程序在遇到当前应用程序域(AppDomain)里没有的程序集时,会自动搜索与应用程序在同一个目录里的所有程序集,看是否有该程序集,如果没有,则会抛出异常。所以如果当我们的动态程序集放到和应用程序在同一个目录里时,不会产生序列化和反序列化的问题,而一旦我们把各种功能的程序集放到不同的内层文件夹时,在处理反序列化的时候需要额外的一些操作才能实现。之后通过一个简单的例子来讲述动态程序集的序列化和反序列化。
动态加载程序集的示例
首先,新建一个主项目(Console Application)并命名为TestApp,该程序用来加载动态程序集。在主程序里定义一个接口IPersonControl,让动态程序集实现这个接口,就能方便的调用动态程序集的方法。接口很简单,就一个函数Print(),即打印输出。
using System; namespace TestApp { public interface IPersonControl { void Print(); } }
OK,现在来创建一个程序集,新建一个Class Library项目,命名为DynamicAssembly,然后添加对TestApp的引用。之后我们写一个简单的Person类,只包含姓名和年龄两个变量和属性,同时实现IPersonControl接口的方法,输出姓名和年龄。类的定义如下
using System; using TestApp; namespace DynamicAssembly { [Serializable] public class Person : IPersonControl { private string name; public string Name { get { return name; } set { name = value; } } private int age; public int Age { get { return age; } set { age = value; } } public Person(string name, int age) { this.name = name; this.age = age; } public void Print() { Console.WriteLine("name:{0} age:{1}", name, age); } } }
好了,这个简单的程序集就写好了,Build一下,然后在bin/Debug目录里就看到了DynamicAssembly.dll这个编译好的文件,将其拷贝到主程序项目(TestApp)所在目录的bin/Debug目录中,好让TestApp动态调用。
这个时候,我们集中精力到TestApp上,在Main函数中动态加载DynamicAssembly.dll,然后创建Person类的实例,并调用其Print方法,首先引用处理反射的类库
using System.Reflection;
然后添加修改Main函数如下
static void Main(string[] args) { //动态加载程序集DynamicAssembly.dll Assembly myAssembly = Assembly.LoadFrom("DynamicAssembly.dll"); //获得程序集里的所有类 Type[] types = myAssembly.GetTypes(); //找到程序集里的Person类 Type myType = null; foreach (Type type in types) { if (type.Name == "Person") { myType = type; } } //创建这个动态类的实例 object o = Activator.CreateInstance(myType, new object[] { "shinichi_wtn", 22 }); //执行类中的Print方法 IPersonControl iPerson = o as IPersonControl; iPerson.Print(); Console.Read(); }
运行程序,输出为
name:shinichi_wtn age:22
因此,动态加载程序集并且新建对象是很容易的一件事情。
动态程序集的序列化
有时候,我们更希望将这个对象保存下来,即序列化操作,我们添加两个函数,分别处理序列化和反序列化操作(using System.Runtime.Serialization.Formatters.Binary),如下
static void SerializeObject(object o, string path) { BinaryFormatter binFormatter = new BinaryFormatter(); using (FileStream fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None)) { binFormatter.Serialize(fs, o); } } static object DeserializeObject(string path) { BinaryFormatter binFormatter = new BinaryFormatter(); object o; using (FileStream fs = new FileStream(path, FileMode.Open)) { o = binFormatter.Deserialize(fs); } return o; }
在Main函数中添加序列化的操作
... //创建这个动态类的实例 object o = Activator.CreateInstance(myType, new object[] { "shinichi_wtn", 22 }); //执行类中的Print方法 IPersonControl iPerson = o as IPersonControl; iPerson.Print(); //反序列化对象到本地 SerializeObject(o, "person.dat"); ...
再运行一下,就在bin/Debug目录里得到了对象的序列化文件person.dat
动态程序集的反序列化
之后,我们测试从person.dat还原到Person对象的操作,修改Main函数如下
static void Main(string[] args) { //还原序列化的对象 object o = DeserializeObject("person.dat"); //执行类中的Print方法 IPersonControl iPerson = o as IPersonControl; iPerson.Print(); Console.Read(); }
运行程序,输出为
name:shinichi_wtn age:22
这个时候,比较好奇,因为在程序中并没有添加对DynamicAssembly.dl的引用,也没有动态加载,而事实是居然能直接被反序列化,可以看到在执行程序的时候,遇到未知程序集的时候,主程序会自动寻找同一目录下的程序集,如果有该未知程序集,那么就被自动加载了。可以用一个简单的方法来测试这个猜想。
static void Main(string[] args) { Console.WriteLine("---反序列化前---"); foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()) { Console.WriteLine(assembly.FullName); } Console.WriteLine("---反序列化后---"); //还原序列化的对象 object o = DeserializeObject("person.dat"); foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()) { Console.WriteLine(assembly.FullName); } Console.Read(); }
运行后发现,反序列化前并没有输出DynamicAssembly.dll,而反序列化后就有了,说明DynamicAssembly.dll是在程序运行时自动被加载的。这就引发一个问题,当DynamicAssembly.dll不在主程序目录下时,就会抛出异常。我们把DynamicAssembly.dll放到lib目录里,然后再运行程序,直接异常了……
在开发较大的软件时,模块和类库可能会很多,不可能将所有dll文件都放在和主程序一个目录里,因此必须要处理当动态程序集不能被程序自动加载的时候该怎么操作。这可以通过AppDomain里的AssemblyResolve事件来完成,这个事件在应用程序域找不到相应程序集时触发,如果我们在事件中指定程序集的位置,那么就能被应用程序域调用。OK,代码如下(using System.ComponentModel;)
static void Main(string[] args) { //添加找不到程序集的处理函数 AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve); //反序列化对象到本地 object o = DeserializeObject("person.dat"); //执行类中的Print方法 IPersonControl iPerson = o as IPersonControl; iPerson.Print(); Console.Read(); } static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs e) { //手动指定程序集的位置 return Assembly.LoadFrom("lib\\DynamicAssembly.dll"); }
运行程序,输出为
name:shinichi_wtn age:22
问题解决了,即通过手动指定程序集能够解决在反序列化的时候找不到程序集的错误。有了这个基础,我们在序列化和反序列化的时候变得更加灵活,这在开发一个开放式的软件,有着插件/组建/模块功能的时候,可以对动态加载的程序集创建对象、并执行序列化和反序列化操作,的确是一件很好的事情。