一、基本概念
反射:反射是一个运行库类型发现的过程。 通过反射可以得到一个给定程序集所包含的所有类型的列表,这个列表包括给定类型中定义的方法、字段、属性和事件。也可以动态的发现一组给定类支持的借口、方法的参数和其他相关信息如基类、命名空间、数据清单等。
二、命名空间
1.System.Reflection命名空间内的各类型
(1)Assembly 通过它可以加载、了解和操纵一个程序集
(2) AssemblyName 通过它可以找到大量隐藏在程序集的身份中的信息,如版本信息、区域信息等
(3) EventInfo 事件的信息
(4) FieldInfo 字段的信息
(5) MethodInfo 方法的信息
(6) ParameterInfo 参数的信息
(7)PropertyInfo 属性的信息
(8) MemberInfo 是抽象基类,为 EventInfo、FieldInfo 、MethodInfo、PropertyInfo等类型定义了公共的行为。
(9) Module 用来访问带有多文件程序集的给定模块
2.System.Type类
System.Type支持的成员可以分为这样几类
(1) Is*** 用来检查一个类型的元数据,如IsAbstract、IsClass、IsValueType等等
(2) Get*** 用来从类型得到指定项目,如GetEvent()得到类型的一个指定的事件(EventInfo)。 另外,这些方法都有一个单数版本和一个复数版本。如GetEvent()对应有一个复数版 本GetEvents(), 该方法返回一个相关的EventInfo数组
(3) FindMembers() 根据查询条件返回一个MemberInfo类型的数组
(4)GetType() 该静态方法根据一个字符串名称返回一个Type实例
(5)InvokeMember() 对给定项目进行晚期绑定
3.得到一个Type类型实例的三种方法(因为Type是一个抽象类,所以不能直接使用new关键字创建一个Type对象)
(1) 使用System.Object.GetType()
e.g: Person pe=new Person(); //---------定义pe为person类的一个对象
Type t=pe.GetType();
(2)使用System.Type.GetType()静态方法,参数为类型的完全限定名
e.g: Type t=Type.GetType("Entity.Person");
该方法被重载,允许指定两个布尔类型的参数,一个用来控制当前类型不能找到时是否抛出异常, 另一个用来指示是否区分字符串大小写
e.g: Type t=Type.GetType("Entity.Person",false,true); 注意到传入的字符串并没有包含类型所在的程序集信息,此时该类型便被认为是定义在当前执行的程序集中的。 要得到一个外部私有程序集的类型元数据时,字符串参数必须使用类型完全限定名加上类型所在程序集的友好名字
e.g: Type t=Type.GetType("Entity.Person","Entity");//------"Entity"即为类型所在程序集的友好名字 ,\.
嵌套类型:传入的字符串可以指定一个+标记来表示一个嵌套类型,如希望得到一个嵌套在person类中的枚举类型City的类型信息,
则可以这样 e.g: Type t=Type.GetType("Entity.person+City");
(3) 使用typeof运算符 e.g: Type t=typeof(person);
三种方法的比较:使用第一种方法必须先建立一个实例,而后两种方法不必先建立实例。但使用typeof运算符仍然需要知道类型的编译时信息,而使用System.Type.GetType()静态方法,不需要知道类型的编译时信息,所以是首选方法。
♧----------------♧--------------♧--------------♧--------♧------------♧----------♧----------------------♧--------------♧下面是一个实例,简单的运用了前面介绍的知识,实现了对一个Type对象的反射,包括反射其所有可见字段、方法、属性、事件。反射类型的基本属性。并将其中一个方法的详细信息列了出来
【源代码】
2using System.Collections.Generic;
3using System.Linq;
4using System.Text;
5using System.Reflection;
6
7using System.Collections; //--------要实现IEnumerable接口则必须制定该命名空间
8
9
10namespace Exercise
11{
12 public class BasePerson //-------------假设一个基类,定义了一个公共方法和一个私有方法
13 {
14 public void BasePublic()
15 {
16 }
17
18 private void BasePrivate()
19 {
20 }
21
22 };
23
24
25 //Person类实现了接口IEnumerable,使得类中定义的Array数组能够使用foreach枚举
26 public class Person :BasePerson, IEnumerable
27 {
28 private string name = "林田惠"; //---姓名
29 public int age=20; //---年龄
30
31 Array children=null;//---子女数组
32
33 Person()
34 {
35 }
36
37 Person(string a,int b)
38 {
39 Name = a;
40 Age = b;
41 }
42
43
44 public string Name
45 {
46 get { return name; }
47 set { }
48 }
49
50 public int Age
51 {
52 get { return age; }
53 set { }
54 }
55
56 public void AddAge()//---自增一岁的方法
57 {
58 Age+=1;
59 }
60
61
62 public delegate void PersonNameHandler(string x);
63 public event PersonNameHandler OnChangeName; //------定义了一个改变姓名的事件
64
65 public void ChangeName(string nam)//---改名的方法
66 {
67 Name = nam;
68 }
69
70 public void ChangeNameAndAddAge(string name,int age)//------具有两个参数的方法,用来演示反射具体方法的详细情况
71 {
72 this.Name = name;
73 this.Age += age;
74 }
75
76 public IEnumerator GetEnumerator()//---实现接口
77 {
78 return children.GetEnumerator();
79 }
80
81 }
82
83
84
85 public class Program
86 {
87
88
89 构建自定义元数据查看器#region 构建自定义元数据查看器
90
91 //-------------显示传入类型拥有的方法名称
92 public static void ListMethods(Type t)
93 {
94 Console.WriteLine("\n该类型的所有方法:");
95 MethodInfo[] mi = t.GetMethods();
96 foreach (MethodInfo m in mi)
97 {
98 Console.WriteLine("\t方法名:{0}",m.Name);
99 }
100 }
101
102
103 //-------------显示一个方法的所有参数信息
104 public static void ListParameters(MethodInfo m)
105 {
106 ParameterInfo[] pi = m.GetParameters();
107 foreach (ParameterInfo p in pi)
108 {
109 Console.WriteLine("参数名:{0}\t参数类型:{1}",p.Name,p.ParameterType);
110 }
111 }
112
113
114 //-------------显示一个特定方法的详细情况
115 public static void ListMethodDetail(Type t, string MethodName)
116 {
117 MethodInfo m = t.GetMethod(MethodName);
118 Console.WriteLine("\n显示方法详情\n方法名称:{0}", MethodName);
119 Console.WriteLine("方法返回值类型:{0}", m.ReturnType.FullName);
120 ListParameters(m);
121
122 }
123
124 //------------显示传入类型拥有的字段名称
125 public static void ListFields(Type t)
126 {
127 Console.WriteLine("\n该类型的所有字段:");
128 FieldInfo [] fi = t.GetFields();
129 foreach (FieldInfo f in fi )
130 {
131 Console.WriteLine("\t字段名:{0}", f.Name);
132 }
133 }
134
135 //------------显示传入类型拥有的属性名称
136 public static void ListProperties(Type t)
137 {
138 Console.WriteLine("\n该类型的所有属性:");
139 PropertyInfo[] pi = t.GetProperties();
140 foreach (PropertyInfo p in pi)
141 {
142 Console.WriteLine("\t属性名:{0}", p.Name);
143 }
144 }
145
146
147 //------------显示传入类型拥有的基类名称
148 public static void ListInterFaces(Type t)
149 {
150 Console.WriteLine("\n该类型的所实现的接口:");
151 Type[] ii = t.GetInterfaces(); //-----------对GetInterfaces()的调用返回一个 Type类型 的数组(说明其实接口也是一种类型)
152 foreach (Type p in ii)
153 {
154 Console.WriteLine("\t接口名:{0}", p.Name);
155 }
156 }
157
158 //------------显示传入类型拥有的属性名称
159 public static void ListEvents(Type t)
160 {
161 Console.WriteLine("\n该类型的所有事件名:");
162 EventInfo [] ei = t.GetEvents();
163 foreach (EventInfo e in ei )
164 {
165 Console.WriteLine("\n事件名:{0}", e.Name);
166 }
167 }
168
169 //------------各种其他信息
170 public static void ListOtherInfo(Type t)
171 {
172 Console.WriteLine("基类名称:{0}",t.BaseType);
173 Console.WriteLine("基类的基类的名称:{0}", t.BaseType.BaseType);
174 Console.WriteLine("是一个类吗?:{0}", t.IsClass);
175 Console.WriteLine("是一个抽象类吗?:{0}", t.IsAbstract);
176 }
177
178
179 #endregion
180
181 public static void Main(string[] args)
182 {
183
184 Console.WriteLine("----------------------------------------------------------------");
185
186 Type t = Type.GetType("Exercise.Person");//-------Person类的完全限定名为"Exercise.Person"
187
188 ListOtherInfo(t);//反射其他一些信息
189 ListFields(t);//反射字段
190 ListProperties(t);//反射属性
191 ListInterFaces(t);//反射接口
192 ListEvents(t);//反射事件
193 ListMethodDetail(t, "ChangeNameAndAddAge");//反射一个特定方法的详细信息
194 ListMethods(t);//反射方法
195
196
197 Console.ReadLine();
198 }
199 }
200}
201
202
【实现效果】
【总结】结合源代码和运行效果,总结如下
1.Name属性在编译后成为了get_Name()和set_Name()两个独立的方法
2.OnChangeName事件的注册(+=)和取消注册(-=)分别成为了add_ OnChangeName ()和remove_ OnChangeName方法
3.私有(private)字段name 没有被打印出来
4.基类的基类System.Object的成员GetType()和Equals()也被打印了出来,基类的共有方法也被打印出来
为了更好的控制显示我们所想要的信息,下面简单介绍一下FindMembers() 方法。
MemberInfo[] mi = t.FindMembers( //【 FindMembers 】
MemberTypes.Method, //【说明查找的成员类型为 Method】
BindingFlags.Public |
BindingFlags.Static |
BindingFlags.NonPublic | //【位屏蔽】
BindingFlags.Instance |
BindingFlags.DeclaredOnly,
Type.FilterName, //【执行比较的委托】
"*"
);
将上例的ListMethods方法改为
public static void ListMethods(Type t)
{
Console.WriteLine(""n该类型的所有方法:");
MemberInfo[] mi = t.FindMembers( //【 FindMembers 】
MemberTypes.Method, //【说明查找的成员类型为 Method】
BindingFlags.Public |
BindingFlags.Static |
BindingFlags.NonPublic | //【位屏蔽】
BindingFlags.Instance |
BindingFlags.DeclaredOnly,
Type.FilterName, //【执行比较的委托】
"*"
);
foreach (MethodInfo m in mi)
{
Console.WriteLine(""t方法名:{0}", m.Name);
}
}
Type.FilterName 返回一个MemberFilter类型的委托,它说明按照方法名称进行过滤,最后一个参数“*”,说明返回所有名称(如果使用“Get*”,则会返回所有以Get开头的方法)
现在的输出如下:
可以看到,所有继承而来的方法都没有显示。。
通过以上实例可以看到System.Reflection命名空间和System.Type类允许我们反射Type实例的大量信息。
然而,对于当前创建的这个实例有一个很大的限制——仅仅能访问当前的程序集,所以接下来要讨论:
如何能使应用程序加载并反射在运行时并不知道的程序集?
1.动态加载程序集
在很多时候我们需要在运行时以编程的方式动态载入程序集,即使那些程序集没有记录在程序清单中。
按需加载外部程序集的操作被称为动态加载
System.Reflection提供了一个名为Assembly的类,使用它我们可以:
(1)动态加载程序集并找到关于程序集自身的属性
(2)动态加载私有或共享程序集
(3)加载任意位置的程序集
从本质上说,Assembly类提供的方法(尤其是Load()和LoadFrom())使得我们可以用编程的方
式提供和客户端.config文件中同样的信息
下面通过一个实例来演示如何通过一个程序集的友好名称来动态加载一个程序集,并打印出其所
包含的每个类、接口、委托等等信息。
【源代码】
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
namespace ActiveLoad
{
class Program
{
//------------定义用来打印程序集中详细情况的函数
static void ListAllTypes(Assembly asm)
{
Console.WriteLine("程序集名称:{0}", asm.FullName);
Type[] types = asm.GetTypes();
foreach (Type t in types)
{
Console.WriteLine("\n类型:{0}\t名称:{1}", t, t.Name);
}
}
static void Main(string[] args)
{
Assembly asm = Assembly.Load("Exercise");//--------根据程序集的友好名称动态加载程序集,ActiveLoad项目必须添加对Exercise项目的引用
//ListAllTypes(asm);
asm = Assembly.Load("LINDERMAN.Entity");
ListAllTypes(asm);
Console.ReadLine();
}
}
}
【运行效果】
注意
2.静态方法Assembly.Load方法仅仅传入了一个要加载到内存的程序集的友好名称,因此如果想
反射Exercise.dll,则需要把Exercise.dll文件复制到ActiveLoad应用程序的"Bin"Debug目录,
然后再来运行这个程序。【此时仍需先在项目中手动添加对Exercise.dll的引用:
添加引用->浏览->ActiveLoad项目的bin文件夹..添加】
3.如果希望让ActiveLoad更加灵活,可以使用Assembly.LoadFrom方法。此时只要在想查看的程序
集前面加上一个绝对路径。
2.晚期绑定
晚期绑定是一种创建一个给定类型的实例并在运行时调用其成员而不需要在编译时知道它存在的一种技术,对于程序的可扩展性来说非常重要。
要介绍晚期绑定技术,首先必须介绍一下System.Activator类。
System.Activator类除了继承自Object的方法外,其本身只定义了几个成员方法,而且其中大多数都与.net远程处理有关
为了建立一个晚期绑定类型的实例,当前我们只需关注Activator.CreateInstance()方法。
CreateInstance()方法经历过多次重载,其中最简单的变化是带有一个有效的Type对象,描述希望动态分配的实体。
该方法将返回一个基本的Object类型而不是一个强类型——并不是我们所传入的类型。
在Exercise.person类内加入打印个人信息的函数
public void DisplayInfo()
{
Console.WriteLine("姓名:{0},年龄:{1}",this.Name,this.Age);
}
下面新建一个LataBinding项目,通过晚期绑定来建立一个Exercise.person类的实例,并调用DisplayInfo()
【源代码】
class Program
{
static void Main(string[] args)
{
Assembly asm = Assembly.Load("Exercise");
Type man = asm.GetType("Exercise.person");
object Man = Activator.CreateInstance(man);//-------返回一个object类型而不是Exercise.person类型
}
}
得到的实例Man是一个Object类型而不是一个Exercise.person类型。且不能通过显示转换来解决问题,因为程序并不知道Exercise.person是什么
注意:晚期绑定的重点是建立编译时未知的对象的实例
下面通过反射来实现打印属性的工作:
首先,使用Type.GetMethod方法
修改源代码如下:
{
static void Main(string[] args)
{
Assembly asm = Assembly.Load("Exercise");
Type man = asm.GetType("Exercise.Person");//--------注意大小写
object Man = Activator.CreateInstance(man);//-------返回一个object类型而不是Exercise.person类型
MethodInfo mi = man.GetMethod("DisplayInfo");
mi.Invoke(Man, null);
Console.ReadLine();
}
}
其中Invoke方法含有两个参数,第一为Object类型,表示调用方法所依赖的实例对象;第二个参数为数组类型,表示调用方法或构造函数所使用的参数,当赋值为NULL时表示调用无参方法。
运行效果
下面再演示一个调用有参数方法的实例:
调用Exercise.person的ChangeNameAndAddAge方法
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
namespace LateBinding
{
class Program
{
static void Main(string[] args)
{
Assembly asm = Assembly.Load("Exercise");
Type man = asm.GetType("Exercise.Person");//--------注意大小写
object Man = Activator.CreateInstance(man);//-------返回一个object类型而不是Exercise.person类型
MethodInfo mi = man.GetMethod("DisplayInfo");
mi.Invoke(Man, null);
mi = man.GetMethod("ChangeNameAndAddAge");
object[] par = new object[2];
par[0] = "林田惠改名了";
par[1]=10;
mi.Invoke(Man, par);//--------执行了改名并增长岁数的方法
mi = man.GetMethod("DisplayInfo");
mi.Invoke(Man, null);//-----改名后再显示个人信息
Console.ReadLine();
}
}
}
【运行效果】
♧----------------♧--------------♧--------------♧--------♧------------♧----------♧----------------------♧--------------♧
3.特性编程
特性就是用于类型(比如类、接口、结构)、成员(比如属性、方法)、程序集或模块的代码注解。
.NET程序员可以使用特性把更多的元数据嵌入到程序集中,用来修饰类型的行为。
当在代码中应用特性时,如果它们没有被另一个软件显式的反射,那么嵌入的元数据基本没什么作用。反之,嵌入程序集中的元数据介绍将被忽略不计,而并无害处。
特性的使用者:1..NET Framework SDK中的许多工具都需要查找各种特性,C#编译器本身就要在编译周期中寻找各种特性是否存在。
2.除了开发工具,在.NET基类库中的许多方法也被设定为要反射指定的特性
3..NET CLR也巡查某些特性是否存在
4.用户可以构建反射自定义的特性和.NET基类库中的特性的应用程序。
C#预定义特性的简单介绍
[CLSCompliant] 强制被注解项遵从CLS
[DllImport] 允许.NET代码调用任意非托管的C或C++基类库,包括操作系统中的API
[Obsolete] 标记一个不用的类或成员
[Serializable] 标记一个类或结构可以被“序列化”
[NonSerialized] 指定类或结构中的某个字段不能在序列化过程中被持久化
[WebMethod] 标记一个方法可以通过HTTP请求调用,并且通知CLR将方法的返回值序列化为XML
注意:1.一个特性只能被应用在紧接下来的对象,例如
[Serializable]
public class Person
{
[NonSerialized]
public float salary;
public bool sex;
public string[] hobbit;
}
在该类中不能被序列化的仅是salary,而由于实体类中注释有[Serializable],所以其他字段都可以被序列化
2.一个项可以被加上多种特性,如下:
[Serializable,
Obsolete("这个类已经过时了,请用更新的版本")]
public class Person
{
public float salary;
……
}
或者这样
[Serializable]
[Obsolete("这个类已经过时了,请用更新的版本")]
public class Person
{
public float salary;
……
}
3.可以为特性指定构造参数
当给特性提供构造参数时,直到该特性被其他类型或外部工具反射后,特性才被分配大内存中,定义在特性级的字符串数据只是作为元数据介绍被存储在程序集中。
也就是说,直到被其他代理反射,特性才发挥使用。
4.C#特性的简化符号
当名称转换时,所有.NET特性,包括自己建立的自定义特性都将加上一个Attribute得后缀,但是为简化使用过程,C#语言不需要输入Attribute后缀。
也即, [SerializableAttribute]
public class Person
{
……
和 [Serializable]
public class Person
{
……
效果是一样的。
自定义特性
(1)构建自定义特性
构建自定义特性的第一步是建立一个新的派生自System.Attribute的类,考虑到安全原因,把所有自定义特性都设计成密封的是一个好习惯
为Person类设计一个特性类如下
public sealed class personAttribute : System.Attribute
{
private string _PersonData;//---定义一个私有字段
public personAttribute()
{
}
public personAttribute(string Data)//---带参数构造函数
{
}
public String PersonData//---维护私有字段的属性
{
get { return _PersonData; }
set { _PersonData = value; }
}
}
(2)使用自定义特性
使用自定义特性有两种方法
1.位置参数:使用自定义特性类的构造函数来为其所维护的私有字段赋值
如: [Exercise.Program.personAttribute("这是在使用位置参数通过构造函数赋值")]
public class Person :BasePerson, IEnumerable
{
private string name = "林田惠"; //---姓名
public int age=20; //---年龄
2.命名参数:使用自定义特性的属性成员来为其所维护的私有字段赋值
如: [Exercise.Program.personAttribute(PersonData="这是在使用位置参数通过构造函数赋值")]
public class Person :BasePerson, IEnumerable
{
private string name = "林田惠"; //---姓名
public int age=20; //---年龄
限制特性的使用
默认情况下自定义特性可以被应用在代码中几乎所有的方面(方法、类、属性等)。
有时希望限定自定义特性的应用范围,则需要在自定义特性的定义中应用[AttributeUsage]特性,[AttributeUsage]特性支持任意AttributeTargets枚举值的组合
例如,我们希望将刚刚定义的特性变成只可应用于类或结构,不能重复应用于一个项,且不能被应用于继承项,则可如下修改定义:
[AttributeUsage(AttributeTargets.Class|AttributeTargets.Struct,
AllowMultiple=false,Inherited=false)]
public sealed class personAttribute : System.Attribute
{
private string _PersonData;//---定义一个私有字段
public personAttribute()
实例运用
实例一 使用早期绑定反射特性
如果希望使用早期绑定,则相应的特性需要客户应用程序在编译时定义
在Exercise命名空间下定义一个属性,将其设为可重复应用于同一个项上,代码如下
namespace Exercise
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct,
AllowMultiple = true, Inherited = false)]
public sealed class personDataAttribute : System.Attribute
{
private string _PersonData;//---定义一个私有字段
public personDataAttribute()
{
}
public personDataAttribute(string Data)//---带参数构造函数
{
_PersonData = Data;
}
public String PersonData//---维护私有字段的属性
{
get { return _PersonData; }
set { _PersonData = value; }
}
}
……
在Person类的定义上应用三个personDataAttribute属性如下:
[personData("这是第一个personData特性")]
[personData("这是第二个personData特性")]
[personData(PersonData = "这是第三个personData特性")]
public class Person :BasePerson, IEnumerable
{
private string name = "林田惠"; //---姓名
public int age=20; //---年龄
Array children=null;//---子女数组
public Person()
{
……
设置Exercise项目为启动项,在Exercise命名空间下,修改Main函数如下
public static void Main(string[] args)
{
/* 演示使用早期绑定反射特性 */
Type t = typeof(Person);
Object[] customAtts = t.GetCustomAttributes(false);//---false代表不搜索继承链
foreach (personDataAttribute pda in customAtts)
{
Console.WriteLine("应用了一个personDataAttribute特性,其中personData={0}",pda.PersonData);
}
Console.ReadLine();
}
运行效果如下:
几点说明:1.Type.GetCustmeAttributes()方法返回一个对象数组,表示了应用到Type代表的成员上的所有特性,包含一个bool参数,指示是否扩展搜索到继承链。
2.该例子中之所以能够使用personDataAttribute特性来对Person类进行描述,是因为在Exercise程序集中personDataAttribute类被定义为公共成员。
3.一个困惑:我在给Person类添加属性时是按照1、2、3的顺序,可代码运行显示的顺序却是2、1、3.。。不知道为什么,还等高手来解答。。。
实例二 使用晚期绑定反射特性
1.先为Exercise程序集生成dll文件(我用的是VS2008):在解决方案资源管理器中右击Exercise项目->属性->应用程序->输出类型->类库
2.将Exercise.dll文件复制到LateBinding项目的Bin"Debug文件夹下
3. 将LateBinding项目的Main函数修改如下:
static void Main(string[] args)
{
/* 以下代码演示晚期绑定反射特性 */
//----加载程序集
Assembly asmb = Assembly.Load("Exercise");
//----得到Exercise.personDataAttribute的类型信息
Type PersonAttr = asmb.GetType("Exercise.personDataAttribute");
//----得到personData属性的类型信息
PropertyInfo proPersonData = PersonAttr.GetProperty("PersonData");
//----得到程序集中的所有类型
Type []types = asmb.GetTypes();
foreach (Type t in types)
{
object[] cusAttrs = t.GetCustomAttributes(PersonAttr,false);
foreach (object o in cusAttrs)
{
Console.WriteLine("特性名称:{0}"t特性值:{1}"n", t.Name, proPersonData.GetValue(o,null));
}
}
Console.ReadLine();
}
运行效果:
说明:一个方法:PropertyInfo.GetValue 用索引化属性的可选索引值返回该属性的值。
public virtual Object GetValue(
Object obj,
Object[] index
)
参数
obj
类型:System.Object
将返回其属性值的对象。
index
类型:System.Object[]
索引化属性的可选索引值。对于非索引化属性,此值应为 null 引用。
返回值
类型:System.Object
obj 参数的属性值。
若要使用 GetValue 方法,请先获取类 Type。从 Type 获取 PropertyInfo。从 PropertyInfo 使用 GetValue 方法
-----------------♧------------------------------------♧------------------------------------♧-------------------
对于C#反射的基础知识,本文由两点未涉及:1.反射共享程序集 2.程序集级别和模块级别特性。