Reflection in C#

Why Reflection

反射是一种计算机在运行时查看和修改对象内部结构与行为的一种能力,它是1982年由Brain Cantwell Smith博士提出的。反射语言的特性最早出现在汇编语言中,汇编语言可以针对指令进行编程,在运行时将指令作为数据对其进行修改,从而修改系统运行的行为。

反射是计算机语言非常有价值的一个特性,具体的说,反射可以获取运行时对象的数据、方法、构造以及行为,并对其进行操作。反射可以在运行时基于某些条件或映射关系加载特定类型,也可以应用于需要理解类型的定义来提供丰富功能的类库,有了它晚绑定成为可能,有了它元编程成为可能,有了它对象的序列化变得简单直接等等。它的存在使得程序设计更加的开放与灵活,减少了很多硬编码的场景,从而能够产生更具表达力,更整洁易维护的代码。

Refelction in C

在.net领域中反射应用在很多场景,比如:

  • 解析路由构建Controller
  • 解析类型中的attribute
  • ModelBinder
  • 对象序列化与反序列化(e.g. Json.net)
  • 对象关系映射(e.g. Fluent Nhibernate)
  • 测试框架(e.g. Xunit Runner, Gallio)
  • 代码质量分析工具(e.g. FxCop, Fortify)
  • Windows视窗系统
  • NLog

要实现反射机制,就要求语言能够表达对象的元数据,提供获得元数据和操作对象的机制以及运行时的上下文。.net应用程序在生成程序集和模块的同时会生成一份所包含的类型数据表,也即元数据信息,包括程序集中定义的类型成员等。C#反射接口则提供了封装程序集、模块和类型的对象,通过这些对象可以轻松的获得元数据表中定义的类型,从而能够实现反射的特性,比如创建类型的实例,将类型绑定到现有对象,或从现有对象中获取类型,调用类型的方法访问其字段和属性并修改之。

.net应用程序中包含程序集(Assembly), 模块(Module)和类型(class),程序集是.net程序的最小运行单位,比如dll,exe文件。程序集包含模块,模块包含类型。

命名空间(namespace)是用来对相关的类型进行逻辑性分组,它与程序集并非一一对应关系。一个程序集可以包含多个命名空间,命名空间也可以存在多个程序集中;它们的关系取决于程序集的设计,理论上是不相互矛盾的。比如System.IO.FileStream是包含在MSCorLib.dll中,而System.IO.FileStreamWatcher则在System.dll程序集中。

1) 发现程序集中的类型及其成员

.net中每个对象都有一个类型(Type),这个类型可以由System.Object对象提供的GetType()非虚方法获得。Type是执行类型和对象反射的起点,通过Type对象可以找到对于该类型元数据的定义,包括成员(MemberInfo), 属性(PropertyInfo), 字段(FieldInfo),方法(MethodInfo), 事件(EventInfo), 构造函数(ConstructorInfo), 参数(ParameterInfo)等。

explore metadata#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class SomeObject{  //Type
public string someName; //Field
public event someEvent; //Event
public string SomeValue{get;set;} //Property

public SomeObject(){ //Constructor
someName = “some name”;
}

public Load(System.Type type){
if(this.GetType() == typeof("Some object") ){ //Verify Type
Assembly.load("Some.dll");
}
}

public void Print(){ //Method
System.Type type = this.GetType();
var fields = type.GetFields();
fields.forEach(…)
}
}

  • 常用的发现类型及其成员的反射接口层次结构如下图所示:

{:height=”300px” width=”400px”}

名字 说明
Assembly 定义和加载程序集,加载程序集清单中列出的模块和类型
MemberInfo 获取成员名称,类型
Type 获得类型
PropertyInfo 获取属性的名称,类型,只读或可写状态,访问修饰符,和当前值
FieldInfo 获取字段的名称,类型,访问修饰符和实现详细信息,和当前值
MethodInfo 获取方法的名称,返回类型,参数,访问修饰符
ParameterInfo 获取参数的名称,类型,位置,输入输出
ConstructorInfo 了解構造函數的名称,参数,访问修饰符
  • Binding Flags - 筛选返回的成员种类

    类型的成员可以分为实例成员,公共成员,非公共成员,静态成员,非基类成员(DeclaredOnly),基类型定义的静态成员等多种。BindingFlags就是这样一个用于描述类型成员种类的枚举类型,它可以在获取类型的成员时对返回结构进行过滤,比如Type.GetMemebers(), Type.GetFields()等。

符号 说明
Default 0x00 代表未指定任何标志的一个占位符
IgnoreCase 0x01 返回与指定字符串匹配的成员(忽略大小写)
DeclaredOnly 0x02 当前类型声明的类型成员,不包括继承的成员
Instance 0x04 实例成员
Static 0x08 静态成员
Public 0x10 公共成员
NonPublic 0x20 非公共成员
FlatternHiearcy 0x40 基类型定义的静态成员

如果在使用这些接口时不指定BindingFlags,那么缺省会返回公共成员,也就是BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static.

2) 构建类型的实例

.net Reflection API中提供了多种方式来构建类型的实例,比如Activiator,Type.InvokeMemeber, ConstructorInfo.Invoke,具体的都可以在文档中查到,下面给出Activator的例子。

Activator.CreateInstance

explore metadata#
1
2
3
4
5
6
7

public SomeObject Load(System.Type type){
if(this.GetType() == typeof("Some object") ){ //Verify Type
return System.Activator.CreateInstance(type);
}
}

3) 调用类型的成员

在获得对象的类型后就可以对类型的成员进行操作,比如修改某个成员的值。这些操作都可以通过类型成员提供的方法进行,具体可参考MSDN

explore metadata#
1
2
3
4
5
6
7
8
9
10
11
12
13
14

public SomeObject Load(System.Type type){
if(this.GetType() == typeof("Some object") ){
var someObject = System.Activator.CreateInstance(type);
var properties = someObject.GetType().GetPrpertyInfo();
properties.ForEach( p=> {
if(p.CanWrite){
p.SetValue(type, new {});
}
});

}
}

几点Concern

反射是很强大的机制,它允许在运行时发现并使用编译时还不了解的类型和成员。但是它也存在一些不可避免的问题。

1) 无法保证类型安全

由于反射在取得类型的时候依赖于字符串,比如Type.GetType(“some object”),所以会丧失编译时的类型安全性。因为一旦名字写错,就会在运行时出错。

2) 速度慢

这是众所周知的问题。因为使用反射在程序集中查找类型匹配的的时候,需要依赖于程序集中的元数据信息,而元数据信息中用字符串标示每个类型和成员,所以查找类型扫描元数据实际就是不断进行字符串搜索,而字符串搜索的算法是相对较慢的,自然速度就会受到影响。另一个调用成员的时候,需要经过打包解包,以及安全检查的操作,也会影响性能问题。

#####参考:

Share Comments