在程序中,当我们需要动态的去加载程序集的时候(将对程序集的引用由编译时推移到运行时),反射是一种很好的选择。反射为.NET类型提供了高度的动态能力,包括:元数据的动态查询、绑定与执行、动态代码生成。常用的反射类型包含在System.Reflection和System.Reflection.Emit,反射包括程序集反射、类型反射、接口反射、类型成员反射。
编译时加载程序集
下面先从一个简单的例子说起,假如我们有一个Point类如下所示:
using System;
public class Point
{
public int x;
public int y;
public void Print()
{
Console.WriteLine("[{0},{1}]",x,y);
}
}
先将其编译成Point.dll文件,然后我们需要在Reflect.cs文件中用到这个类型:
代码如下:
using System;
public class Reflect
{
public static void Main()
{
Point p=new Point();
p.x=100;
p.y=200;
p.Print();
}
}
然后我们编译这个文件,会发现抛出异常,告诉我们不存在Point类型,也就是说Point是一个未知类型,编译器根本不知道它,那怎么办了,这里我们需要在编译时对Point程序集进行引用,这样才能让编译器知道它,在很多应用程序开发框架中在我们编译项目的时候框架会自动进行程序集的引用,但是在这里我们的编译工作是手动进行的,因此我们也需要手动的引用程序集。下面是编译命令:
csc /r:Point.dll Reflect.cs
这样我们在编译Reflect.cs文件的过程中就实现了对Point程序集的引用,这样编译器就识别了程序集中Point类型信息。其实任何类型信息的使用都需要有程序集的引用,不然的话编译器会不认识这个类型,编译就无法通过。我们可能会想,我们在很多时候进行编译的时候并没有对哪个程序集进行引用么?不是没有,而是很多预定义的类型信息编译器会默认帮我们进行引用,不需要我们显示的进行引用,比如mscorlib程序集,它是.NET一个核心程序集,里面包含了很多常用的预定义的类型信息,因此C#编译器在编译的时候会默认对它进行引用,可能我们会想,我们怎么知道一段代码在编译的过程中它到底都引用了哪些程序集了,这里就需要借助于元数据,下面我们通过ildasm工具查看了编译好的Reflect.exe程序集中的元数据,我们只截取了其中一段进行说明。
// Metadata version: v4.0.30319
.assembly extern mscorlib
{
.publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) // .z\V.4..
.ver 4:0:0:0
}
.assembly extern Point
{
.ver 0:0:0:0
}
从这段元数据中我们可以看出,这个程序集中包含了对Point程序集和mscorlib程序集的引用。
利用反射实现延迟加载程序集
在上面所举的例子中,所有对程序集的引用都是在编译时进行的,因此效率会比较高。但是在某些特定的情况下,我们需要对程序集进行延迟加载,即将对程序集的引用由编译时推移到运行时,反射是一种很好的选择,在最开始我们讲过,反射为.NET类型提供了高度的动态能力,包括:元数据的动态查询、绑定与执行、动态代码生成,这些功能的实现都离不开元数据。下面我们看看反射的具体实现。
using System;
using System.Reflection;
class Test
{
public static void Main(string[] args)
{
string assemblyName=args[0];
string typeName=args[1];
string fieldName1=args[2];
string fieldName2=args[3];
string methodName=args[4];
Assembly assembly=Assembly.Load(assemblyName); //手动加载程序集
Type type=assembly.GetType(typeName); //获取程序集中的类型
//查询
MemberInfo[] mis=type.GetMembers(); //获取类型中的成员信息
for(int i=0;i<mis.Length;i++)
{
Console.WriteLine(mis[i]);
}
object obj=Activator.CreateInstance(type); //创建对象实例
//查询字段
FieldInfo field1=type.GetField(fieldName1);
FieldInfo field2=type.GetField(fieldName2);
field1.SetValue(obj,100); //实例成员必须依附于对象实例才能赋值
field2.SetValue(obj,200);
//查询方法
MethodInfo method=type.GetMethod(methodName);
//调用方法
method.Invoke(obj,null); //实例方法必须依附于对象实例才能执行
}
}
这里我们在编译这段代码的时候并没有对Point程序集进行引用,而是将其推移到了运行时。在这段代码中Point类型并不存在,因此我们并不能Point类型来创建对象实例。但是下面的几个操作是针对于对象的实例成员进行的,他们需要依附于对象的实例,因此我们在这里根据在运行时加载进来的类型信息创建了一个对象,但是编译时类型信息是未知的。我们可以通过查看它的元数据得知它编译后并没有对Point程序集进行加载。
那么运行时怎么对程序集进行加载了,这里我们就需要将需要加载的程序集的信息在入口点函数中以参数的形式传递进来,下面是运行时的命令:
在这段命令中我们指出了运行时需要加载的程序集的名称和其中的类型的名称以及类型中的成员信息。由于类型信息在编译时是未知的,因此我们并不清楚在运行时会加载进来什么样的类型信息(需要通过传入的参数确定)。也就是说我们现在所进行的一些操作是针对一个不确定的类型的(如果我们事先并不了解Point的类型信息)。而在上面的代码中我们之所以能够对加载进来类型信息进行这样的处理,是因为我们事先已经对Point类型信息有所了解了,因此我们遵循了这样一个隐式的约定来对类型进行操作。但是在很多情况下我们是并不了解未来加载进来的类型信息它所遵循的约定。因此在运行时,反射需要做很多的校验工作,也就是说把本来应该在编译时做的校验工作都推移到了运行时,比如参数类型的兼容性以及所调用的方法是实例方法还是静态方法,因此反射的效率比较低。
利用接口提高反射效率
那么怎么样才能提高反射的效率了,很简单,我们需要明确约定,也就是说只有满足了约定信息的类型才能被加载进来,也因此我们对类型成员的处理必须是满足了这一约定的,这样双方都有了一个共同的约定。那么我们用什么来实现这一约定了,当然需要用到接口了。
我们对上面的例子进行改进,通过对Point类型的观察,我们可以得到这样一个接口,我们先看接口的实现,并将其编译为IPoint.dll文件。
using System;
public interface IPoint
{
public int X{set;get;}
public int Y{set;get;}
void Print();
}
下面我们来实现Point类,并对其进行编译,注意:在编译时确保对IPoint.dll的引用。
using System;
public class Point :IPoint
{
private int x;
private int y;
public int X
{
set{this.x=value;}
get{return x;}
}
public int Y
{
set{this.y=value;}
get{return y;}
}
public void Print()
{
Console.WriteLine("[{0},{1}]",this.X,this.Y);
}
}
下面我们再来看Reflect类的实现,在对其进行编译时同样需要对IPoint.dll进行引用。
using System;
using System.Reflection;
class Test
{
public static void Main(string[] args)
{
string assemblyName=args[0];
string typeName=args[1];
Assembly assembly=Assembly.Load(assemblyName); //手动加载程序集
Type type=assembly.GetType(typeName); //获取程序集中的类型
IPoint obj=(IPoint)Activator.CreateInstance(type); //通过接口创建对象实例
obj.X=100;
obj.Y=200;
obj.Print();
}
}
最后运行编译后的Reflect.exe文件,运行时同样需要传入程序集信息和类型信息。这里我们看到Point程序集和Reflect程序集在编译时都对IPoint程序集进行了引用,因此在Reflect程序集中,虽然在编译时并没有Point类型信息,但是有IPoint类型信息,因此我们通过这个接口很方便的实现了我们需要的操作,只要未来加载进来了类型是实现了IPoint接口的就可以了,这样在编译时就不需要进行大量的校验工作了,这些工作都还原到了运行时,因此使用接口来实现反射也大大提高了反射的性能。
声明:本人接触.NET时间不长,希望各位路过的高手要是觉得文中有错误的话能及时告诉我,本人力求最做到所有信息的正确性,谢谢。