利用反射动态创建对象 (转自张逸的blog)
前两天我发了一篇文章《通过反射动态实例化对象中出现的一个奇怪问题》,对反射中的某些问题疑惑不解。通过这几天不断查看MSDN,上网查询,现在终于解决了该问题。
在VS.Net中,有很多种方法动态调用对象的构造函数。一是通过Activator类的CreateInstance()方法。这个方法我们在Remoting中也用过。它实际上是在本地或从远程创建对象类型,或获取对现有远程对象的引用。它的方法签名是:public static object CreateInstance(Type);(还有其他重载方法)注意它的返回值为object,MSDN对返回值的描述是:对新创建对象的引用。
二是通过Assembly类的方法CreateInstance()。方法名和前一样,不过它不是静态方法。Assembly是在System.Reflection命名空间中。方法签名:public object CreateInstance(Type);(同样还有其他重载方法)返回值仍然是object,MSDN对返回值的描述是:表示该类型的 Object 的实例,其区域性、参数、联编程序和激活属性设置为空引用(Visual Basic 中为 Nothing),并且 BindingFlags 设置为 Public 或 Instance,或者设置为空引用 (Nothing)(如果没有找到 typeName)。
当然还有其他方法,例如通过MethodInfo获得方法信息后,根据IsConstructor属性,判断是否构造函数,再根据GetParamters()方法获得参数,最后通过Invoke()方法来调用,等等……。大家可以参考MSDN。
在这里,我且把问题简单化,只调用其默认构造函数。通过CreateInstance()方法获得object对象,再转换为实际的自定义对象类型。事实证明,这种转换为出现异常。根本的原因我还弄不清楚,初步的猜测,对于动态加载的assembly,和手动添加的assembly,Framework将两者视为了不同的assembly,即使我们使用的是同一个DLL。
我也注意到Actovator.CreateInstance()返回的是新创建对象的引用。是否是引用再作怪呢?但Assembly.CreateInstance()方法,根据MSDN的描述,返回的是object实例,然而仍然会抛出同样的异常。所以,出现问题的具体原因,我确实无法解释。
确实VS.Net博大精深,很多内在的运行机制我们不得而知。好吧,我们就知其然而不知其所以然吧,至少我现在已经知道了利用反射动态创建对象的解决之道!管它这么多,只要会用就行,退而求其次,也未尝不可。
利用反射动态创建对象,事实上就是通过Assembly动态加载DLL。这里所谓的“对象”,应分为两种类型。不同的类型,解决的办法也不相同。
一、.Net自身提供的类对象,例如Form对象、Control对象。
这也是我们在程序开发中会经常用到的。一般我们开发应用程序,都是将界面定义好。有多少个窗口,有多少个控件,事先做好,放在项目中。但有时作交互设计时,还需要考虑用户的请求。也许用户希望某些窗体能够自己决定加载的时间。也就是说,需要提供运行时加载的功能。这时,就需要通过反射来动态创建对象了。(加载的窗体对象dll,通常是放在配置文件中。在.Net中,有专门的配置文件,它是xml格式。有关配置文件,我希望能专门写一篇文章。在本文,我的例子是固定的加载程序集。)
1、创建要动态加载的窗体对象
首先,创建一个窗体对象FirstForm,这个窗体只有一个控件Lable,来显示窗体的名称。然后我们将它编译为dll文件FirstForm.dll,放在e:\AutoForm中。(要生成Dll文件,而不是exe,请在Solution Explorer(解决方案资源管理器)中的 FirstForms 项目上单击鼠标右键,选择 Properties(属性)。在 Output Type(输出类型)组合框中选择 Class Library(类库)。)
这个对象的程序集名为FirstForm.dll,类型为FirstForm.Form1。
2、创建应用程序,动态加载该对象
启动一个新的 Windows 窗体项目。将其命名为 AutoLoadForm。在新项目中包含的空窗体 Form1 中,将它的 IsMdiContainer 属性更改为 True。这样,该窗体即变成一个 MDI 父窗体。更改窗体的大小,使窗体的长和宽的尺寸大约为默认值的两倍。
将一个面板控件拖动到窗体上,然后设置它的 Dock 属性,使它靠接在窗体的顶部。更改面板的大小,使它的高度大约为 50px。
将一个组合框拖动到面板上。将它命名为 cboForms,然后将它的 DropDownStyle 设置为 DropDownList。
最后,将一个按钮拖动到面板上。将它命名为 btnLoadForm,然后将它的 Text 属性设置为 Load Form。
此时,Form1 应如图 1 所示。
然后为程序添加命名空间:
using System.Reflecton;
单击btnLoadForm控件,写入以下代码:
{
Assembly assembly = Assembly.LoadFrom(@"e:\AutoFormFirstForm.dll");
Type type = assembly.GetType("FirstForm.Form1");
object obj = Activator.CreateInstance(type);
Form formToShow = (Form)obj;
formToShow.MdiParent = this;
formToShow.Show();
}
代码说明:
1)首先是通过Assembly.LoadFrom()来加载dll文件;
2)再通过GetType()来获得要创建的Form类对象的类型。注意,在GetType()方法的参数为类型的名字,为string类型,同时该名字应为类型的FullName,即:命名空间名.类名;
3)然后通过Activator.CreateInstance()方法创建该类型对象,返回object对象。
4)再将该对象强制转换为Form类型。
5)最后调用即可。
运行程序,单击按钮,结果如下:
结论:可以看到,对于.Net自身提供的类对象,我们对它直接强制转换即可。不会出现任何问题。
二、自定义对象
前面已经说过,对于自定义的对象,进行强制转换会抛出异常。因此,我们需要做些变通才行。
我们说,动态加载的Dll和手工添加的dll引用,系统会认为不是同一个Assembly。那么应该怎么解决?想一想,对了,应该使用接口。但是,这里使用接口的方法稍微有点特殊。还是先按步骤来讲解吧。
1、创建一个接口,该接口包括要加载对象类的方法、属性等:
新建一个“类库”项目,取名为AutoObjectInterface:
namespace AutoObjectInterface
{
public interface IAutoObject
{
void Print(string s);
}
}
这个接口很简单,只是提供一个Print()方法而已。
然后将它编译为Dll文件,名为AutoObjectInterface。
2、创建自定义类对象:
新建一个“类库”项目,取名为AutoObject,添加前面创建的接口Dll引用:
namespace AutoObject
{
public class TestObject:AutoObjectInterface.IAutoObject
{
public TestObject()
{
}
public void Print(string s)
{
Console.WriteLine(s);
}
}
}
这个类实现了第一步创建的接口。注意,这里实现的接口不是直接写在该类中,而是独立的Dll。在这个类中,是添加了该接口的dll,然后再实现它。这就是前面说的使用接口的一点特殊性。为什么要这样,是因为后面动态加载时,也要引用该接口Dll。我们动态创建后的对象,正是转换为该接口对象。由于实际的类和动态创建的类都引用并实现了该接口Dll,因此它的转换才能成功。这正是实现的关键!
也许有人疑问:我们能否将接口就放在要创建的类中,然后实现它。编译成dll文件,然后动态加载该dll,同时也手动添加该dll。动态创建后的对象,再强制转换为这个接口类型,不可以吗?答案当然是否定的,为什么?别问我,我也不知道!总之,我现在讲的方法,才是通过反射动态创建自定义对象的不二法门!!
也许会有人说我太武断!如果你不信,去试试。如果用另外的方法能成功,我一定改正错误。至少现在我能这样武断。
言归正传。现在我们再将给类编译为dll。名为AutoObject.dll,放到e:\NewObject中。
3、利用反射动态创建该对象:
新建一个控制台项目,取名为StudyReflection,添加前面创建的接口Dll引用。代码如下:
using System.Reflection;
namespace studyReflection
{
class Class1
{
// 应用程序的主入口点。
[STAThread]
static void Main(string[] args)
{
Assembly assembly = Assembly.LoadFrom(@"e:NewObjectAutoObject.dll");
Type type = assembly.GetType("AutoObject.TestObject");
object obj = Activator.CreateInstance(type);
AutoObjectInterface.IAutoObject iObj = (AutoObjectInterface.IAutoObject)obj;
iObj.Print("wayfarer");
Console.ReadLine();
}
}
}
说明:这个代码和前面创建.Net自身提供的对象差不多,关键的区别就是强制转换。因为是自定义对象,所以我们不知道转换为什么对象啊,所以要添加接口的引用。转换的时候就转换为接口的类型:AutoObjectInterface.IAutoObject iObj = (AutoObjectInterface.IAutoObject)obj;
这样我们就可以通过接口对象实例来调用类对象的方法Print()了。运行后,一切OK。
结论:在通过反射动态创建对象时,一定要注意区别所创建对象的类型。如果是自定义对象,必须通过单独的接口,来进行类型的转换。