使用元数据完整地描述类型(类、接口、结构、枚举和委托)的能力是.NET平台的一个关键要素,像对象序列化、WCF等技术都需要在运行时通过元数据来发现类型格式。
通过ildasm.exe这个工具,我们可以查看一个程序集的元数据(Ctrl+M组合键)。在.NET中,利用反射(reflection)服务,我们就可以通过编程方式得到与ildasm.exe显示的相同的元数据信息。
System.Reflection这个命名空间中包括了我们使用反射中涉及的所有类型,但是开始介绍反射之前,我们要看看System.Type类。
System.Type类
System.Type类定义了很多成员,用来检查某个类型的元数据,它们返回的类型大多数都是System.Reflection命名空间中的类型。例如说,Type.GetMethods()返回一个MethodInfo类型的数组,Type.GetFields()返回一个FieldInfo类型的数组等等。
在.NET中,有三种方式可以得到一个Type类的实例引用。
使用System.Object.GetType()
由于Type是一个抽象类,所以不能直接使用new关键字创建一个Type对象,我们的首选方案是使用System.Object定义的GetType()方法,这个方法返回一个表示当前对象元数据的Type类的实例。
Student stu = new Student(); Type studentType = stu.GetType();
通过上面代码也可以看到,使用这个方法获得Type类的实例是有前提的,必须得到类型(Student类)的编译时信息,并且内存中要有该类新的实例(stu实例)。
使用typeof()
在程序中,我们还可以通过C#的typeof操作符类获取类型信息。
Type type = typeof(Student);
使用typeof操作符,我们就不需要先建立一个实例来提取类型信息,但是,仍然需要知道类型的编译时信息,因为typeof需要通过强类型来获取类型信息。
使用System.Type.GetType()
在.NET还有一种更灵活的方式得到类型信息,可以通过System.Type类的静态成员GetType(),然后指定类型的完全限定名。采用这种方法,我们可以不需要知道类型的编译时信息,GetType()方法可以接受任何字符串值(而不是强类型),当然这个字符串就是类型名字的字符串形式。
Type.GetType()方法有很多重载,我们介绍一个常用的:
public static Type GetType(string typeName, bool throwOnError, bool ignoreCase);
使用这个方法我们可以指定两个布尔类型的参数,一个用来表示当类型找不到时是否抛出异常,另一个用来表示是否区分字符串大小写,下面就是第三种获得Student的Type类实例的方式:
Type type = Type.GetType("ReflectionTest.Student", false, true);
在上面这个语句中,字符串没有包含类型所在的程序集信息,这种情况就被认为该类型是定义在当前执行的程序集中。当需要得到一个外部程序集的类型元数据时,字符串参数必须使用类型完全限定名,加上类型所在程序集的友好名字。
Type type = Type.GetType("ReflectionTest.Student, ReflectionTest", false, true);
反射
为了进一步了解反射以及System.Type,下面看一个例子,在这个例子中,我们将使用一些System.Reflection中常被用到的类型,例如MethodInfo,FieldInfo和PropertyInfo等。
namespace ReflectionTest { class Student : IComparable { public int Id { get; private set; } public string Name { get; set; } public int Age { get; set; } public string Gender { get; set; } public void Greeting(string name) { Console.WriteLine("{0} said Hello to {1}", this.Name, name); } public string GetInfo() { return string.Format("Basic Info:\n Name: {0}\n Age: {1}\n Gender: {2}", this.Name, this.Age, this.Gender); } public int CompareTo(object obj) { throw new NotImplementedException(); } } class Program { static void Main(string[] args) { Type type = typeof(Student); ListMethods(type); ListFields(type); ListProps(type); ListInterfaces(type); ListOhterStatus(type); Console.Read(); } static void ListMethods(Type t) { Console.WriteLine("------Methods------"); var methodInfos = from m in t.GetMethods() select m; foreach (var methodInfo in methodInfos) { //Get return type string retVal = methodInfo.ReturnType.FullName; string paramInfo = "("; // Get params foreach (ParameterInfo pi in methodInfo.GetParameters()) { paramInfo += string.Format("{0} {1}", pi.ParameterType, pi.Name); } paramInfo += ")"; Console.WriteLine("-->{0} {1} {2}", retVal, methodInfo.Name, paramInfo); } Console.WriteLine(); } static void ListFields(Type t) { Console.WriteLine("------Fields------"); var fieldNames = from f in t.GetFields() select f.Name; foreach(var fieldName in fieldNames) Console.WriteLine("-->{0}", fieldName); Console.WriteLine(); } static void ListProps(Type t) { Console.WriteLine("------Properties------"); var propNames = from p in t.GetProperties() select p.Name; foreach (var propName in propNames) Console.WriteLine("-->{0}", propName); Console.WriteLine(); } static void ListInterfaces(Type t) { Console.WriteLine("------Interfaces------"); var interfaceNames = from i in t.GetInterfaces() select i.Name; foreach (var interfaceName in interfaceNames) Console.WriteLine("-->{0}", interfaceName); Console.WriteLine(); } static void ListOhterStatus(Type t) { Console.WriteLine("------Other status------"); Console.WriteLine("Full name is: {0}", t.FullName); Console.WriteLine("Base class is: {0}", t.BaseType); Console.WriteLine("Is type abstract? {0}", t.IsAbstract); Console.WriteLine("Is type a class type? {0}", t.IsClass); Console.WriteLine(); } } }
例子还是比较简单的,我们通过反射查看了Student类型的一些元数据信息,结果如下,通过这个例子大致了解到反射的基本使用。
动态加载程序集
在程序的运行中,我们有时会需要加载外部程序集,这个操作被称作动态加载。
通过System.Reflection中的Assembly类型,我们就可以动态的加载程序集,并找到关于程序集的相关特性。通过Assembly类提供的Load()和LoadFrom()方法,我们可以通过代码动态的加载一个程序集。
注意,当使用Load()时,需要把要加载的程序集放到ExternalAssemblyTest工程的"bin/debug"目录,这样Load()方法才能找到这个程序集;当我们使用LoadFrom()方法的时候就比较灵活了,我们可以指定要加载程序集的绝对路径。下面看一个简单的例子:
namespace ExternalAssemblyTest { class Program { static void Main(string[] args) { try { Assembly asm = Assembly.Load("ReflectionTest"); //Assembly asm = Assembly.LoadFrom(@"C:\WorkSpace\VS\SharpInDepth\ExternalAssemblyTest\bin\Debug\ReflectionTest.exe"); DisplayTypesInAssembly(asm); } catch (Exception e) { Console.WriteLine(e.Message); } Console.Read(); } static void DisplayTypesInAssembly(Assembly asm) { Console.WriteLine("------Types in Assembly------"); Console.WriteLine("-->{0}", asm.FullName); var types = from t in asm.GetTypes() select t; foreach (var type in types) Console.WriteLine("Type: {0}", type); } } }
通过这个例子我们可以到了两种动态加载程序集的方法调用,通过反射,我们可以查看被加载程序集的详细信息。
晚期绑定
晚期绑定(late binding)是一种创建一个给定类型的实例,并在运行时调用其成员,而不需要在编译时知道改类型存在的一种技术。
System.Activator类
System.Activator类是.NET晚期绑定过程中的一个关键类型。在代码中,可以通过System.Activator类的CreateInstance静态方法(CreateInstance有很多重载形式,下面给出了一个常用形式)来创建一个晚期绑定类型的实例。
public static object CreateInstance(Type type)
修改上面的例子,这次先动态加载程序集ReflectionTest,然后通过晚期绑定方式为程序集中的Student类型创建一个实例。
static void Main(string[] args) { try { Assembly asm = Assembly.Load("ReflectionTest"); //Assembly asm = Assembly.LoadFrom(@"C:\WorkSpace\VS\SharpInDepth\ExternalAssemblyTest\bin\Debug\ReflectionTest.exe"); //Get Student Type Type studentType = asm.GetType("ReflectionTest.Student"); //Create Student object with late binding object student = Activator.CreateInstance(studentType); Console.WriteLine("Create object {0} with late binding", student); ListMethods(studentType); } catch (Exception e) { Console.WriteLine(e.Message); } Console.Read(); } static void ListMethods(Type t) { Console.WriteLine("------Methods------"); var methodInfos = from m in t.GetMethods() select m; foreach (var methodInfo in methodInfos) { //Get return type string retVal = methodInfo.ReturnType.FullName; string paramInfo = "("; // Get params foreach (ParameterInfo pi in methodInfo.GetParameters()) { paramInfo += string.Format("{0} {1}", pi.ParameterType, pi.Name); } paramInfo += ")"; Console.WriteLine("-->{0} {1} {2}", retVal, methodInfo.Name, paramInfo); } Console.WriteLine(); }
代码的输出为
对于已经创建的student实例,我们可以通过反射来调用实例的方法。
调用没有参数的方法
根据前面了解到,当我们得到一个Type实例后,就可以通过Type.GetMethod()得到一个MethodInfo对象;然后就可以通过MethodInfo对象的Invoke()方法来实现一个方法的调用。
举例,可以在例子中加入下面代码来调用Student类型的GetInfo()方法,由于该方法不需要参数,所有Invoke()的时候用一个null。
//Get the method MethodInfo getInfoMethod = studentType.GetMethod("GetInfo"); //call the method without argument //null here shows that the method has no argument string info = (string)getInfoMethod.Invoke(student, null); Console.WriteLine(info);
代码的输出为,由于没有给Name和Gender属性设置值,所以这里显示为空。
调用有参数的方法
在使用晚期绑定调用需要参数的方法时,要将参数打包到一个object类型的数组中。
下面我们调用属性的set方法给属性赋值。
//Get the method MethodInfo setName = studentType.GetMethod("set_Name"); //call the method without argument //create object array with values of arguments setName.Invoke(student, new object[] { "Wilber" }); MethodInfo setAge = studentType.GetMethod("set_Age"); setAge.Invoke(student, new object[] { 28 }); MethodInfo setGender = studentType.GetMethod("set_Gender"); setGender.Invoke(student, new object[] { "Male" });
程序输出为
总结
通过这篇文章,介绍了反射的一些基本概念,并且通过一些例子简单的演示了反射的使用。通过文章中所有的介绍,应该就可以对反射有个基本的认识了。
接下来一篇文章会介绍一下C#特性(Attribute)跟反射的结合使用。