反射
大多数程序都要处理数据,包括读、写、操作和显示数据。然而,对于某些程序来说,它们操作的数据不是数字、文本或图形,而是程序和程序类型本身的信息。
- 有关程序及其类型的数据被称为元数据(metadata),它们保存在程序的程序集中。
- 程序在运行时,可以查看其他程序集或其本身的元数据。一个运行的程序查看本身的元数据或其他程序的元数据的行为叫做反射(reflection)。
对象浏览器是显示元数据的程序的一个示例。它可以读取程序集,然后显示所包含的类型以及类型的所有特性和成员。
二次编译
C#在编译运行时,会有二次编译的过程:
1、先生成exe或dll文件:
源代码被编译为一种符合 CLI 规范的中间语言 (IL)。IL 代码与资源(例如位图和字符串)一起作为一种称为程序集的可执行文件存储在磁盘上,通常具有的扩展名为 .exe 或 .dll。程序集包含清单(即元数据metadata),它提供有关程序集的类型、版本、区域性和安全要求等信息。另外,也可以通过ILSpy等反编译工具解析出原始代码。
2、把exe或dll文件编译成二进制代码。
执行 C# 程序时,程序集将加载到 CLR 中,这可能会根据清单中的信息执行不同的操作。然后,如果符合安全要求,CLR 就会执行实时 (JIT) 编译以将 IL 代码转换为本机机器指令。CLR 还提供与自动垃圾回收、异常处理和资源管理有关的其他服务。由 CLR 执行的代码有时称为“托管代码”,它与编译为面向特定系统的本机机器语言的“非托管代码”相对应。下图阐释了 C# 源代码文件、.NET Framework 类库、程序集和 CLR 的编译时与运行时的关系。
通过反射,可以在运行时获得程序或程序集中每一个类型(包括类、结构、委托、接口和枚举等)的成员和成员的信息。有了反射,即可对每一个类型了如指掌。另外我还可以直接创建对象,即使这个对象的类型在编译时还不知道。
.Net的应用程序由几个部分:‘程序集(Assembly)’、‘模块(Module)’、‘类型(class)’组成,而反射提供一种编程的方式,让程序员可以在程序运行期获得这几个组成部分的相关信息,例如:
Assembly类可以获得正在运行的装配件信息,也可以动态的加载装配件,以及在装配件中查找类型信息,并创建该类型的实例。
Type类可以获得对象的类型信息,此信息包含对象的所有要素:方法、构造器、属性等等,通过Type类可以得到这些要素的信息,并且调用之。
MethodInfo包含方法的信息,通过这个类可以得到方法的名称、参数、返回值等,并且可以调用之。诸如此类,还有FieldInfo、EventInfo等等,这些类都包含在System.Reflection命名空间下。
反射用到的命名空间:
System.Reflection
System.Type
System.Reflection.Assembly
System.Type类
基础类库(BCL)声明了一个叫做Type的抽象类,它被设计用来包含类型的特性。使用这个类的对象能让我们获取程序使用的类型的信息。
由于Type是抽象类,因此它不能有实例。而是在运行时,CLR创建从Type( Runtime Type)派生的类的实例,Type包含了类型信息。当我们要访问这些实例时,CLR不会返回派生类的引用而是Type基类的引用。但是,为了简单起见我会把引用所指向的对象称为Type类型的对象(虽然从技术角度来说是一个BCL内部的派生类型的对象)。
需要了解的有关Type的重要事项如下。
- 对于程序中用到的每一个类型,CLR都会创建一个包含这个类型信息的Type类型的对象口程序中用到的每一个类型都会关联到独立的Type类的对象。
- 不管创建的类型有多少个实例,只有一个Type对象会关联到所有这些实例。
获取给定类型的Type引用有3种常用方式:
- 使用 C# typeof 运算符。
Type t = typeof(string);
- 使用对象GetType()方法。
string s = "grayworm";
Type t = s.GetType();
- 还可以调用Type类的静态方法GetType()。
Type t = Type.GetType("System.String");
在取出string类型的Type引用t后,我们就可以通过t来探测string类型的结构了。
string n = "grayworm";
Type t = n.GetType();
foreach (MemberInfo mi in t.GetMembers())
{
Console.WriteLine("{0}/t{1}",mi.MemberType,mi.Name);
}
- Type类的属性:
Name 数据类型名
FullName 数据类型的完全限定名(包括命名空间名)
Namespace 定义数据类型的命名空间名
IsAbstract 指示该类型是否是抽象类型
IsArray 指示该类型是否是数组
IsClass 指示该类型是否是类
IsEnum 指示该类型是否是枚举
IsInterface 指示该类型是否是接口
IsPublic 指示该类型是否是公有的
IsSealed 指示该类型是否是密封类
IsValueType 指示该类型是否是值类型 - Type类的方法:
GetConstructor(), GetConstructors():返回ConstructorInfo类型,用于取得该类的构造函数的信息
GetEvent(), GetEvents():返回EventInfo类型,用于取得该类的事件的信息
GetField(), GetFields():返回FieldInfo类型,用于取得该类的字段(成员变量)的信息
GetInterface(), GetInterfaces():返回InterfaceInfo类型,用于取得该类实现的接口的信息
GetMember(), GetMembers():返回MemberInfo类型,用于取得该类的所有成员的信息
GetMethod(), GetMethods():返回MethodInfo类型,用于取得该类的方法的信息
GetProperty(), GetProperties():返回PropertyInfo类型,用于取得该类的属性的信息
可以调用这些成员,其方式是调用Type的InvokeMember()方法,或者调用MethodInfo, PropertyInfo和其他类的Invoke()方法。
用反射生成对象,并调用属性、方法和字段进行操作
NewClassw nc = new NewClassw();
Type t = nc.GetType();
object obj = Activator.CreateInstance(t);
//取得ID字段
FieldInfo fi = t.GetField("ID");
//给ID字段赋值
fi.SetValue(obj, "k001");
//取得MyName属性
PropertyInfo pi1 = t.GetProperty("MyName");
//给MyName属性赋值
pi1.SetValue(obj, "grayworm", null);
PropertyInfo pi2 = t.GetProperty("MyInfo");
pi2.SetValue(obj, "hi.baidu.com/grayworm", null);
//取得show方法
MethodInfo mi = t.GetMethod("show");
//调用show方法
mi.Invoke(obj, null);
System.Reflection.Assembly类
Assembly类可以获得程序集的信息,也可以动态的加载程序集,以及在程序集中查找类型信息,并创建该类型的实例。使用Assembly类可以降低程序集之间的耦合,有利于软件结构的合理化。
创建Assembly类
一个通过程序集名称返回Assembly对象并进行调用的例子如下:
Assembly assembly = Assembly.Load("ClassLibrary831");//dll名称无后缀;从当前目录加载 //1.通过程序集名称返回Assembly对象
//Assembly assembly2 = Assembly.LoadFrom("ClassLibrary831.dll");//带后缀或者完整路径
Type type = assembly.GetType("Ruanmou.DB.MySql.MySqlHelper");//2.获取类型信息
object oDBHelper = Activator.CreateInstance(type);//3.创建对象
IDBHelper iDBHelper = (IDBHelper)oDBHelper;//4.类型转换
iDBHelper.Query();//5.方法调用
一般可以通过工厂模式创建对象,如下:
using Ruanmou.DB.Interface;
using System;
using System.Configuration;
using System.Reflection;
namespace MyReflection
{
/// <summary>
/// 创建对象
/// </summary>
public class Factory
{
private static string IDBHelperConfig = ConfigurationManager.AppSettings["IDBHelperConfig"];
private static string DllName = IDBHelperConfig.Split(',')[1];
private static string TypeName = IDBHelperConfig.Split(',')[0];
public static IDBHelper CreateHelper()//1 2
{
Assembly assembly = Assembly.Load(DllName);//1 加载dll
Type type = assembly.GetType(TypeName);//2 获取类型信息
object oDBHelper = Activator.CreateInstance(type);//3 创建对象
IDBHelper iDBHelper = (IDBHelper)oDBHelper;//4 类型转换
return iDBHelper;
}
}
}
这里我们可以把相关的配置放在配置文件App.config中,并通过Configuration类读取,然后,我们就可以通过工厂来调用
IDBHelper iDBHeler = Factory.CreateHelper();//1/2
iDBHeler.Query();//可配置可扩展 反射是动态的 依赖的是字符串
看起来这样比直接调用方法更麻烦,为什么还需要这么做呢?这样的好处是,我们可以通过修改配置文件App.config直接生效,不需要再修改代码,重新编译,即可配置可扩展。这就是IOC(Inversion of Control),即“控制反转”的思想。
通过反射破坏单例
通过反射可以达到普通调用方法不能实现的目的。例如,可以通过反射破坏单例,调用私有的构造函数。
从本质上讲,单例是一个只允许创建自身的单个实例的类,并且通常可以简单地访问该实例。通常,单例的要求是它们是懒惰地创建的——即直到第一次需要时才创建实例。通常通过一个静态变量,用于保存对单个已创建实例的引用(如果有的话)。
一个简单的单例实现如下:
namespace Ruanmou.DB.SqlServer
{
/// <summary>
/// 单例模式
/// </summary>
public sealed class Singleton
{
private static Singleton _Singleton = null;
private Singleton()
{
Console.WriteLine("Singleton被构造");
}
static Singleton()
{
_Singleton = new Singleton();
}
public static Singleton GetInstance()
{
return _Singleton;
}
}
}
一般我们通过多次直接构造的话,只会调用一次构造函数。
Singleton singleton1 = Singleton.GetInstance();
Singleton singleton2 = Singleton.GetInstance();
Singleton singleton3 = Singleton.GetInstance();
但通过反射可以破坏单例,多次调用了私有构造函数,如下:
Type type = assembly.GetType("Ruanmou.DB.SqlServer.Singleton");
Singleton singleton4 = (Singleton)Activator.CreateInstance(type, true);
Singleton singleton5 = (Singleton)Activator.CreateInstance(type, true);
Singleton singleton6 = (Singleton)Activator.CreateInstance(type, true);
通过反射调用函数
假如一个类有多个构造函数的话,我们也可以通过指定object数组指定特定的构造函数。
Type type = assembly.GetType("Ruanmou.DB.SqlServer.ReflectionTest");
object oReflectionTest1 = Activator.CreateInstance(type);
object oReflectionTest2 = Activator.CreateInstance(type, new object[] { 123 });
object oReflectionTest3 = Activator.CreateInstance(type, new object[] { "123" });
分别调用了ReflectionTest(),ReflectionTest(string name),ReflectionTest(int id)三个构造函数。
同样的我们也可以调用泛型类的构造函数。
Type type = assembly.GetType("Ruanmou.DB.SqlServer.GenericClass`3");
//object oGeneric = Activator.CreateInstance(type); //需要指定参数类型
Type newType = type.MakeGenericType(new Type[] { typeof(int), typeof(string), typeof(DateTime) });
object oGeneric = Activator.CreateInstance(newType);
另外,反射也可以直接找到方法,并指定和调用方法。如下:
Assembly assembly = Assembly.Load("Ruanmou.DB.SqlServer");
Type type = assembly.GetType("Ruanmou.DB.SqlServer.ReflectionTest");
object oReflectionTest = Activator.CreateInstance(type);
MethodInfo method = type.GetMethod("Show1");
method.Invoke(oReflectionTest, null);
//调用重载方法
MethodInfo method = type.GetMethod("Show2");
method.Invoke(oReflectionTest, new object[] { 123 });
//调用私有方法
MethodInfo method = type.GetMethod("Show4", BindingFlags.Instance | BindingFlags.NonPublic);
method.Invoke(oReflectionTest, new object[] { "name" })
//调用泛型方法
Type typeGenericDouble = assembly.GetType("Ruanmou.DB.SqlServer.GenericDouble`1");
Type newType = typeGenericDouble.MakeGenericType(new Type[] { typeof(int) });
object oGeneric = Activator.CreateInstance(newType);
MethodInfo method = newType.GetMethod("Show");
MethodInfo methodNew = method.MakeGenericMethod(new Type[] { typeof(string), typeof(DateTime) });
methodNew.Invoke(oGeneric, new object[] { 123, "name", DateTime.Now });
通过反射调用方法,可以方便的在方法执行前后来添加处理过程,这也是MVC中通过URL地址--类名称+方法名称来指定调用API的实现方法。
通过反射读取属性和字段
另外,反射也可以直接操作属性和字段,最常用的一种做法是动态通过某个类的值实现另外一个类,如下:
Type typePeople = typeof(People);
Type typePeopleDTO = typeof(PeopleDTO);
object peopleDTO = Activator.CreateInstance(typePeopleDTO);
foreach (var prop in typePeopleDTO.GetProperties())
{
object value = typePeople.GetProperty(prop.Name).GetValue(people);
prop.SetValue(peopleDTO, value);
}
foreach (var filed in typePeopleDTO.GetFields())
{
object value = typePeople.GetField(filed.Name).GetValue(people);
filed.SetValue(peopleDTO, value);
}
这就是对象关系映射(Object-Relational Mapping)的实现方式,即可以创建一个可在编程语言里使用的--“虚拟对象数据库”。
反射的优缺点
反射具有很多优点:
- 可以使用反射动态地创建类型的实例,将类型绑定到现有对象,或从现有对象中获取类型
- 应用程序需要在运行时从某个特定的程序集中载入一个特定的类型,以便实现某个任务时可以用到反射。
- 反射主要应用于类库,这些类库需要知道一个类型的定义,以便提供更多的功能。
但反射写起来十分复杂,并且具有性能问题,在应用时我们需要注意:
- 现实应用程序中很少有应用程序需要使用反射类型
- 使用反射动态绑定需要牺牲性能
- 有些元数据信息是不能通过反射获取的
- 某些反射类型是专门为那些clr 开发编译器的开发使用的,所以你要意识到不是所有的反射类型都是适合每个人的。
使用反射来调用类型或者触发方法,或者访问一个字段或者属性时clr 需要做更多的工作:校验参数,检查权限等等,所以速度是非常慢的。所以尽量不要使用反射进行编程,对于打算编写一个动态构造类型(晚绑定)的应用程序,可以采取以下的几种方式进行代替:
- 通过类的继承关系。让该类型从一个编译时可知的基础类型派生出来,在运行时生成该类型的一个实例,将对其的引用放到其基础类型的一个变量中,然后调用该基础类型的虚方法。
- 通过接口实现。在运行时,构建该类型的一个实例,将对其的引用放到其接口类型的一个变量中,然后调用该接口定义的虚方法。
- 通过委托实现。让该类型实现一个方法,其名称和原型都与一个在编译时就已知的委托相符。在运行时先构造该类型的实例,然后在用该方法的对象及名称构造出该委托的实例,接着通过委托调用你想要的方法。这个方法相对与前面两个方法所作的工作要多一些,效率更低一些。