.Net 中的反射(反射特性) - Part.3
转自:http://www.cnblogs.com/JimmyZhang/archive/2008/01/27/1055254.html
反射特性(Attribute)
可能很多人还不了解特性,所以我们先了解一下什么是特性。想想看如果有一个消息系统,它存在这样一个方法,用来将一则短消息发送给某人:
// title: 标题;author:作者;content:内容;receiverId:接受者Id
public bool SendMsg(string title, string author, string content, int receiverId){
// Do Send Action
}
我们很快就发现这样将参数一个个罗列到方法的参数列表中扩展性很糟糕,我们最好定义一个Message类将短消息封装起来,然后给方法传递一个Message对象:
public class Message{
private string title;
private string author;
private string content;
private int receiverId;
// 略
}
public bool SendMsg(Messag msg){
// Do some Action
}
此时,我们或许应该将旧的方法删除,用这个扩展性更好的SendMsg方法来取代。遗憾的是我们往往不能,因为这组程序可能作为一组API发布,在很多客户程序中已经在使用旧版本的SendMsg()方法,如果我们在更新程序的时候简单地删除掉旧的SendMsg()方法,那么将造成使用老版本SendMsg()方法的客户程序不能工作。
这个时候,我们该如果做呢?我们当然可以通过方法重载来完成,这样就不用删除旧的SendMsg()方法了。但是如果新的SendMsg()不仅优化了参数的传递,并且在算法和效率上也进行了全面的优化,那么我们将会迫切希望告知客户程序现在有一个全新的高性能SendMsg()方法可供使用,但此时客户程序并不知道已经存在一个新的SendMsg方法,我们又该如何做呢?我们可以打电话告诉维护客户程序的程序员,或者发电子邮件给他,但这样显然不够方便,最好有一种办法能让他一编译项目,只要存在对旧版本SendMsg()方法的调用,就会被编译器告知。
1..Net内置特性介绍
.Net 中可以使用特性来完成这一工作。特性是一个对象,它可以加载到程序集及程序集的对象中,这些对象包括 程序集本身、模块、类、接口、结构、构造函数、方法、方法参数等,加载了特性的对象称作特性的目标。特性是为程序添加元数据(描述数据的数据)的一种机制,通过它可以给编译器提供指示或者提供对数据的说明。
NOTE:特性的英文名称叫做Attribute,在有的书中,将它翻译为“属性”;另一些书中,将它翻译为“特性”;由于通常我们将含有get和/或set访问器的类成员称为“属性”(英文Property),所以本文中我将使用“特性”这个名词,以区分“属性”(Property)。
中文版的VS2005使用“属性”。
1.1 System.ObsoleteAttribute 特性
我们通过这个例子来看一下特性是如何解决上面的问题:我们可以给旧的SendMsg()方法上面加上Obsolete特性来告诉编译器这个方法已经过时,然后当编译器发现当程序中有地方在使用这个用Obsolete标记过的方法时,就会给出一个警告信息。
namespace Attribute {
public class Message {}
public class TestClass {
// 添加Obsolete特性
[Obsolete("请使用新的SendMsg(Message msg)重载方法")]
public static void ShowMsg() {
Console.WriteLine("这是旧的SendMsg()方法");
}
public static void ShowMsg(Message msg) {
Console.WriteLine("新SendMsg()方法");
}
}
class Program {
static void Main(string[] args) {
TestClass.ShowMsg();
TestClass.ShowMsg(new Message());
}
}
}
现在运行这段代码,我们会发现编译器给出了一个警告:警告CS0618: “Attribute.TestClass.ShowMsg()”已过时:“请使用新的SendMsg(Message msg)重载方法”。通过使用特性,我们可以看到编译器给出了警告信息,告诉客户程序存在一个新的方法可供使用,这样,程序员在看到这个警告信息后,便会考虑使用新的SendMsg()方法。
NOTE:简单起见,TestClass类和 Program位于同一个程序集中,实际上它们可以离得很远。
1.2 特性的使用方法
通过上面的例子,我们已经大致看到特性的使用方法:首先是有一对方括号“[]”,在左方括号“[”后紧跟特性的名称,比如Obsolete,随后是一个圆括号“()”。和普通的类不同,这个圆括号不光可以写入构造函数的参数,还可以给类的属性赋值,在Obsolete的例子中,仅传递了构造函数参数。
NOTE:实际上,当你用鼠标框选住Obsolete,然后按下F12转到定义,会发现它的全名是ObsoleteAttribute,继承自Attribute类。但是这里却仅用Obsolete来标记方法,这是.Net的一个约定,所有的特性应该均以Attribute来结尾,在为对象标记特性时如果没有添加Attribute,编译器会自动寻找带有Attribute的版本。
NOTE:使用构造函数参数,参数的顺序必须同构造函数声明时的顺序相同,所有在特性中也叫位置参数(Positional Parameters),与此相应,属性参数也叫做命名参数(Named Parameters)。在下面会详细说明。
2.自定义特性(Custom Attributes)
2.1 范例介绍
如果不能自己定义一个特性并使用它,我想你怎么也不能很好的理解特性,我们现在就自己构建一个特性。假设我们有这样一个很常见的需求:我们在创建或者更新一个类文件时,需要说明这个类是什么时候、由谁创建的,在以后的更新中还要说明在什么时候由谁更新的,可以记录也可以不记录更新的内容,以往你会怎么做呢?是不是像这样在类的上面给类添加注释:
//更新:Matthew, 2008-2-10, 修改 ToString()方法
//更新:Jimmy, 2008-1-18
//创建:张子阳, 2008-1-15
public class DemoClass{
// Class Body
}
这样的的确确是可以记录下来,但是如果有一天我们想将这些记录保存到数据库中作以备份呢?你是不是要一个一个地去查看源文件,找出这些注释,再一条条插入数据库中呢?
通过上面特性的定义,我们知道特性可以用于给类型添加元数据,这些元数据可以用于描述类型。那么在此处,特性应该会派上用场。那么在本例中,元数据应该是:注释类型(“更新”或者“创建”),修改人,日期,备注信息(可有可无)。而特性的目标类型是DemoClass类。
按照对于附加到DemoClass类上的元数据的理解,我们先创建一个封装了元数据的类RecordAttribute:
public class RecordAttribute {
private string recordType; // 记录类型:更新/创建
private string author; // 作者
private DateTime date; // 更新/创建 日期
private string memo; // 备注
// 构造函数,构造函数的参数在特性中也称为“位置参数”。
public RecordAttribute(string recordType, string author, string date) {
this.recordType = recordType;
this.author = author;
this.date = Convert.ToDateTime(date);
}
// 对于位置参数,通常只提供get访问器
public string RecordType { get { return recordType; } }
public string Author { get { return author; } }
public DateTime Date { get { return date; } }
// 构建一个属性,在特性中也叫“命名参数”
public string Memo {
get { return memo; }
set { memo = value; }
}
}
NOTE:注意构造函数的参数 date,必须为一个常量、Type类型、或者是常量数组,所以不能直接传递DateTime类型。
这个类不光看上去,实际上也和普通的类没有任何区别,显然不能它因为名字后面跟了个Attribute就摇身一变成了特性。那么怎样才能让它称为特性并应用到一个类上面呢?进行下一步之前,我们看看.Net内置的特性Obsolete是如何定义的:
namespace System {
[Serializable]
[AttributeUsage(6140, Inherited = false)]
[ComVisible(true)]
public sealed class ObsoleteAttribute : Attribute {
public ObsoleteAttribute();
public ObsoleteAttribute(string message);
public ObsoleteAttribute(string message, bool error);
public bool IsError { get; }
public string Message { get; }
}
}
2.2 添加特性的格式(位置参数和命名参数)
首先,我们应该发现,它继承自Attribute类,这说明我们的RecordAttribute也应该继承自Attribute类。
其次,我们发现在这个特性的定义上,又用了三个特性去描述它。这三个特性分别是:Serializable、AttributeUsage 和 ComVisible。Serializable特性我们前面已经讲述过,ComVisible简单来说是“控制程序集中个别托管类型、成员或所有类型对 COM 的可访问性”(微软给的定义)。这里我们应该注意到:特性本身就是用来描述数据的元数据,而这三个特性又用来描述特性,所以它们可以认为是“元数据的元数据”(元元数据:meta-metadata)。
因为我们需要使用“元元数据”去描述我们定义的特性 RecordAttribute,所以现在我们需要首先了解一下“元元数据”。这里应该记得“元元数据”也是一个特性,大多数情况下,我们只需要掌握 AttributeUsage就可以了,所以现在就研究一下它。我们首先看上面AttributeUsage是如何加载到ObsoleteAttribute特性上面的。
[AttributeUsage(6140, Inherited = false)]
然后我们看一下AttributeUsage的定义:
namespace System {
public sealed class AttributeUsageAttribute : Attribute {
public AttributeUsageAttribute(AttributeTargets validOn);
public bool AllowMultiple { get; set; }
public bool Inherited { get; set; }
public AttributeTargets ValidOn { get; }
}
}
可以看到,它有一个构造函数,这个构造函数含有一个AttributeTargets类型的位置参数(Positional Parameter),还有两个命名参数(Named Parameter)。注意ValidOn属性不是一个命名参数,因为它不包含set访问器。
这里大家一定疑惑为什么会这样划分参数,这和特性的使用是相关的。假如AttributeUsageAttribute 是一个普通的类,我们一定是这样使用的:
// 实例化一个 AttributeUsageAttribute 类
AttributeUsageAttribute usage=new AttributeUsageAttribute(AttributeTargets.Class)
;
usage.AllowMultiple = true; // 设置AllowMutiple属性
usage.Inherited = false;// 设置Inherited属性
但是,特性只写成一行代码,然后紧靠其所应用的类型(目标类型),那么怎么办呢?微软的软件工程师们就想到了这样的办法:不管是构造函数的参数 还是 属性,统统写到构造函数的圆括号中,对于构造函数的参数,必须按照构造函数参数的顺序和类型;对于属性,采用“属性=值”这样的格式,它们之间用逗号分隔。于是上面的代码就减缩成了这样:
[AttributeUsage(AttributeTargets.Class, AllowMutiple=true, Inherited=false)]
可以看出,AttributeTargets.Class是构造函数参数(位置参数),而AllowMutiple 和 Inherited实际上是属性(命名参数)。命名参数是可选的。将来我们的RecordAttribute的使用方式于此相同。(为什么管他们叫参数,我猜想是因为它们的使用方式看上去更像是方法的参数吧。)
假设现在我们的RecordAttribute已经OK了,则它的使用应该是这样的:
[RecordAttribute("创建","张子阳","2008-1-15",Memo="这个类仅供演示")]
public class DemoClass{
// ClassBody
}
其中recordType, author 和 date 是位置参数,Memo是命名参数。
2.3 AttributeTargets 位标记
从AttributeUsage特性的名称上就可以看出它用于描述特性的使用方式。具体来说,首先应该是其所标记的特性可以应用于哪些类型或者对象。从上面的代码,我们看到AttributeUsage特性的构造函数接受一个 AttributeTargets 类型的参数,那么我们现在就来了解一下AttributeTargets。
AttributeTargets 是一个位标记,它定义了特性可以应用的类型和对象。
[Flags]
public enum AttributeTargets {
Assembly = 1, //可以对程序集应用属性。
Module = 2, //可以对模块应用属性。
Class = 4, //可以对类应用属性。
Struct = 8, //可以对结构应用属性,即值类型。
Enum = 16, //可以对枚举应用属性。
Constructor = 32, //可以对构造函数应用属性。
Method = 64, //可以对方法应用属性。
Property = 128, //可以对属性 (Property) 应用属性 (Attribute)。
Field = 256, //可以对字段应用属性。
Event = 512, //可以对事件应用属性。
Interface = 1024, //可以对接口应用属性。
Parameter = 2048, //可以对参数应用属性。
Delegate = 4096, //可以对委托应用属性。
ReturnValue = 8192, //可以对返回值应用属性。
GenericParameter = 16384, //可以对泛型参数应用属性。
All = 32767, //可以对任何应用程序元素应用属性。
}
现在应该不难理解为什么上面我范例中用的是:
[AttributeUsage(AttributeTargets.Class, AllowMutiple=true, Inherited=false)]
而ObsoleteAttribute特性上加载的 AttributeUsage是这样的:
[AttributeUsage(6140, Inherited = false)]
因为AttributeUsage是一个位标记,所以可以使用按位或“|”来进行组合。所以,当我们这样写时:
[AttributeUsage(AttributeTargets.Class|AttributeTargets.Interface)
意味着既可以将特性应用到类上,也可以应用到接口上。
NOTE:这里存在着两个特例:观察上面AttributeUsage的定义,说明特性还可以加载到程序集Assembly和模块Module上,而这两个属于我们的编译结果,在程序中并不存在这样的类型,我们该如何加载呢?可以使用这样的语法:[assembly:SomeAttribute(parameter list)],另外这条语句必须位于程序语句开始之前。
2.4 Inherited 和 AllowMutiple属性
AllowMutiple 属性用于设置该特性是不是可以重复地添加到一个类型上(默认为false),就好像这样:
[RecordAttribute("更新","Jimmy","2008-1-20")]
[RecordAttribute("创建","张子阳","2008-1-15",Memo="这个类仅供演示")]
public class DemoClass{
// ClassBody
}
所以,我们必须显示的将AllowMutiple设置为True。
Inherited 就更复杂一些了,假如有一个类继承自我们的DemoClass,那么当我们将RecordAttribute添加到DemoClass上时,DemoClass的子类也会获得该特性。而当特性应用于一个方法,如果继承自该类的子类将这个方法覆盖,那么Inherited则用于说明是否子类方法是否继承这个特性。
在我们的例子中,将 Inherited 设为false。
2.5 实现 RecordAttribute
现在实现RecordAttribute应该是非常容易了,对于类的主体不需要做任何的修改,我们只需要让它继承自Attribute基类,同时使用AttributeUsage特性标记一下它就可以了(假定我们希望可以对类和方法应用此特性):
[AttributeUsage(AttributeTargets.Class|AttributeTargets.Method, AllowMultiple=true, Inherited=false)]
public class RecordAttribute:Attribute {
// 略
}
2.6 使用 RecordAttribute
我们已经创建好了自己的自定义特性,现在是时候使用它了。
[Record("更新", "Matthew", "2008-1-20", Memo = "修改 ToString()方法")]
[Record("更新", "Jimmy", "2008-1-18")]
[Record("创建", "张子阳", "2008-1-15")]
public class DemoClass {
public override string ToString() {
return "This is a demo class";
}
}
class Program {
static void Main(string[] args) {
DemoClass demo = new DemoClass();
Console.WriteLine(demo.ToString());
}
}
这段程序简单地在屏幕上输出一个“This is a demo class”。我们的属性也好像使用“//”来注释一样对程序没有任何影响,实际上,我们添加的数据已经作为元数据添加到了程序集中。可以通过IL DASM看到:
3.使用反射查看自定义特性
利用反射来查看 自定义特性信息 与 查看其他信息 类似,首先基于类型(本例中是DemoClass)获取一个Type对象,然后调用Type对象的GetCustomAttributes()方法,获取应用于该类型上的特性。当指定GetCustomAttributes(Type attributeType, bool inherit) 中的第一个参数attributeType时,将只返回指定类型的特性,否则将返回全部特性;第二个参数指定是否搜索该成员的继承链以查找这些属性。
class Program {
static void Main(string[] args) {
Type t = typeof(DemoClass);
Console.WriteLine("下面列出应用于 {0} 的RecordAttribute属性:" , t);
// 获取所有的RecordAttributes特性
object[] records = t.GetCustomAttributes(typeof(RecordAttribute), false);
foreach (RecordAttribute record in records) {
Console.WriteLine(" {0}", record);
Console.WriteLine(" 类型:{0}", record.RecordType);
Console.WriteLine(" 作者:{0}", record.Author);
Console.WriteLine(" 日期:{0}", record.Date.ToShortDateString());
if(!String.IsNullOrEmpty(record.Memo)){
Console.WriteLine(" 备注:{0}",record.Memo);
}
}
}
}
输出为:
下面列出应用于 AttributeDemo.DemoClass 的RecordAttribute属性:
AttributeDemo.RecordAttribute
类型:更新
作者:Matthew
日期:2008-1-20
备注:修改 ToString()方法
AttributeDemo.RecordAttribute
类型:更新
作者:Jimmy
日期:2008-1-18
AttributeDemo.RecordAttribute
类型:创建
作者:张子阳
日期:2008-1-15
好了,到了这一步,我想将这些数据录入数据库中将不再是个问题,我们关于反射自定义特性的章节也就到此为止了。
posted @ 2009-10-19 10:29 大漠银狐 阅读(17) 评论(0) 编辑
.Net 中的反射(查看基本类型信息) - Part.2
转自:http://www.cnblogs.com/JimmyZhang/archive/2008/02/17/1071372.html
反射概述 和Type类
1.反射的作用
简单来说,反射提供这样几个能力:1、查看和遍历类型(及其成员)的基本信息和程序集元数据(metadata);2、迟绑定(Late-Binding)方法和属性。3、动态创建类型实例(并可以动态调用所创建的实例的方法、字段、属性)。序章中,我们所采用的那个例子,只是反射的一个用途:查看类型成员信息。接下来的几个章节,我们将依次介绍反射所提供的其他能力。
2.获取Type对象实例
反射的核心是Type类,这个类封装了关于对象的信息,也是进行反射的入口。当你获得了关于类型的Type对象后,就可以根据Type提供的属性和方法获取这个类型的一切信息(方法、字段、属性、事件、参数、构造函数等)。我们开始的第一步,就是获取关于类型的Type实例。获取Type对象有两种形式,一种是获取当前加载程序集中的类型(Runtime),一种是获取没有加载的程序集的类型。
我们先考虑Runtime时的Type,一般来说有三种获取方法:
2.1使用Type类提供的静态方法GetType()
比如我们想要获得Stream类型的Type实例,则可以这样:
Type t = Type.GetType("System.IO.Stream");
txtOutput.Text = t.ToString();
注意到GetType方法接受字符串形式的类型名称。
2.2 使用 typeof 操作符
也可以使用C# 提供的typeof 操作符来完成这一过程:
// 如果在页首写入了using System.IO; 也可以直接用 typeof(Stream);
Type t = typeof(System.IO.Stream);
这时的使用有点像泛型,Stream就好像一个类型参数一样,传递到typeof操作符中。
2.3 通过类型实例获得Type对象
我们还可以通过类型的实例来获得:
String name = "Jimmy Zhang";
Type t = name.GetType();
使用这种方法时应当注意,尽管我们是通过变量(实例)去获取Type对象,但是Type对象不包含关于这个特定对象的信息,仍是保存对象的类型(String)的信息。
3.Type类型 及 Reflection命名空间的组织结构
到现在为止,我已经多次提过Type封装了类型的信息,那么这些类型信息都包含什么内容呢?假设我们现在有一个类型的实例,它的名字叫做 demo,我们对它的信息一无所知,并通过下面代码获取了对于它的Type实例:
// 前面某处的代码实例化了demo对象
Type t = demo.GetType();
现在,我们期望 t 包含了关于 demo 的哪些信息呢?
3.1 demo的类型的基本信息
- 我们当然首先想知道 demo 是什么类型的,也就是 demo 的类型名称。
- 我们还想知道该类型位于什么命名空间下。
- 它的基类型是什么,以及它在.Net运行库中的映射类型。
- 它是值类型还是引用类型。
- 它是不是Public的。
- 它是枚举、是类、是数组、还是接口。
- 它是不是基础类型(int等)。
- 等等 ...
Type 提供了下面的属性,用于获取类型的基本信息,常用的有下面一些:
属 性
说 明
Name
获取类型名称
FullName
类型全名
Namespace
命名空间名称
BaseType
获取对于基类的Type类型的引用
UnderlyingSystemType
在.Net中映射的类型的引用
Attributes
获取TypeAttributes位标记
IsValueType
是否值类型
IsByRef
是否由引用传递
IsEnum
是否枚举
IsClass
是否类
IsInterface
是否接口
IsSealed
是否密封类
IsPrimitive
是否基类型(比如int)
IsAbstract
是否抽象
IsPublic
是否公开
IsNotPublic
是否非公开
IsVisible
是否程序集可见
等等...
3.2 demon的类型的成员信息
- 我们可能还想知道它有哪些字段。
- 有些什么属性,以及关于这些属性的信息。
- 有哪些构造函数。
- 有哪些方法,方法有哪些参数,有什么样的返回值。
- 包含哪些事件。
- 实现了哪些接口。
- 我们还可以不加区分地获得它的所有 以上成员。
观察上面的列表,就拿第一条来说,我们想获取类型都有哪些字段,以及这些字段的信息。而字段都包含哪些信息呢?可能有字段的类型、字段的名称、字段是否public、字段是否为const、字段是否是read only 等等,那么是不是应该将字段的这些信息也封装起来呢?
实际上,.Net中提供了 FiledInfo 类型,它封装了关于字段的相关信息。对照上面的列表,类似的还有 PropertyInfo类型、ConstructorInfo类型、MethodInfo类型、EventInfo类型。而对于方法而言,对于它的参数,也会有in参数,out参数,参数类型等信息,类似的,在 System.Reflection 命名空间下,除了有上面的提到的那么多Info后缀结尾的类型,还有个ParameterInfo 类型,用于封装方法的参数信息。
最后,应该注意到 Type 类型,以及所有的Info类型均 继承自 MemberInfo 类型,MemberInfo类型提供了获取类型基础信息的能力。
在VS2005中键入Type,选中它,再按下F12跳转到Type类型的定义,纵览Type类型的成员,发现可以大致将属性和方法分成这样几组:
- IsXXXX,比如 IsAbstract,这组bool属性用于说明类型的某个信息。(前面的表格已经列举了一些。)
- GetXXXX(),比如GetField(),返回FieldInfo,这组方法用于获取某个成员的信息。
- GetXXXXs(),比如GetFields(),返回FieldInfo[],这组方法用户获取某些成员信息。
- 还有其他的一些属性和方法,等后面遇到了再说。
由于MemberInfo是一个基类,当我们获得一个MemberInfo后,我们并不知道它是PropertyInfo(封装了属性信息的对象)还是FieldInfo(封装了属性信息的对象),所以,有必要提供一个办法可以让我们加以判断,在Reflection 命名空间中,会遇到很多的位标记,这里先介绍第一个位标记(本文管用[Flags]特性标记的枚举称为 位标记),MemberTypes,它用于标记成员类型,可能的取值如下:
[Flags]
public enum MemberTypes {
Constructor = 1, // 该成员是一个构造函数
Event = 2, // 该成员是一个事件
Field = 4, // 该成员是一个字段
Method = 8, // 该成员是一个方法
Property = 16, // 该成员是一个属性
TypeInfo = 32, // 该成员是一种类型
Custom = 64, // 自定义成员类型
NestedType = 128, // 该成员是一个嵌套类型
All = 191, // 指定所有成员类型。
}
反射程序集
在.Net中,程序集是进行部署、版本控制的基本单位,它包含了相关的模块和类型,我并不打算详细地去说明程序集及其构成,只是讲述如何通过反射获取程序集信息。
在System.Reflection命名空间下有一个Assembly类型,它代表了一个程序集,并包含了关于程序集的信息。
在程序中加载程序集时,一般有这么几个方法,我们可以使用 Assembly类型提供的静态方法LoadFrom() 和 Load(),比如:
Assembly asm = Assembly.LoadFrom("Demo.dll");
或者
Assembly asm = Assembly.Load("Demo");
当使用LoadFrom()方法的时候,提供的是程序集的文件名,当将一个程序集添加到项目引用中以后,可以直接写“文件名.dll”。如果想加载一个不属于当前项目的程序集,则需要给出全路径,比如:
Assembly asm = Assembly.LoadFrom(@"C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\System.Web.dll");
使用Load()方法的时候,只用提供程序集名称即可,不需要提供程序集的后缀名。如果想获得当前程序集,可以使用Assembly类型的静态方法 GetExecutingAssembly,它返回包含当前执行的代码的程序集(也就是当前程序集)。
Assembly as = Assembly.GetExecutingAssembly();
在获得一个Type类型实例以后,我们还可以使用该实例的Assembly属性来获得其所在的程序集:
Type t = typeof(int)
Assembly asm = t.Assembly;
一个程序集可能有多个模块(Module)组成,每个模块又可能包含很多的类型,但.Net的默认编译模式一个程序集只会包含一个模块,我们现在看下 反射 提供了什么样的能力让我们获取关于程序集的信息(只列出了部分常用的):
属 性/方 法
说 明
FullName
程序集名称
Location
程序集的路径
GetTypes()
获取程序集包含的全部类型
GetType()
获取某个类型
GetModules()
获取程序集包含的模块
GetModule()
获取某个模块
GetCustomAttributes()
获取自定义特性信息
NOTE:程序集和命名空间不存在必然联系,一个程序集可以包含多个命名空间,同一个命名空间也可以分放在几个程序集。
为了方便进行我们后面的测试,我们现在建立一个Windows控制台应用程序,我给它起名叫SimpleExplore;然后再添加一个Demo类库项目,我们将来编写的代码就用户查看这个Demo项目集的类型信息 或者 是对这个程序集中的类型进行迟绑定。这个Demon项目只包含一个命名空间Demo,为了体现尽可能多的类型同时又Keep Simple,其代码如下:
namespace Demo {
public abstract class BaseClass {
}
public struct DemoStruct { }
public delegate void DemoDelegate(Object sender, EventArgs e);
public enum DemoEnum {
terrible, bad, common=4, good, wonderful=8
}
public interface IDemoInterface {
void SayGreeting(string name);
}
public interface IDemoInterface2 {}
public sealed class DemoClass:BaseClass, IDemoInterface,IDemoInterface2 {
private string name;
public string city;
public readonly string title;
public const string text = "Const Field";
public event DemoDelegate myEvent;
public string Name {
private get { return name; }
set { name = value; }
}
public DemoClass() {
title = "Readonly Field";
}
public class NestedClass { }
public void SayGreeting(string name) {
Console.WriteLine("Morning :" + name);
}
}
}
现在我们在 SimpleExplore项目中写一个方法AssemblyExplor(),查看我们Demo项目生成的程序集Demo.dll定义的全部类型:
public static void AssemblyExplore() {
StringBuilder sb = new StringBuilder();
Assembly asm = Assembly.Load("Demo");
sb.Append("FullName(全名):" + asm.FullName + "\n");
sb.Append("Location(路径):" + asm.Location + "\n");
Type[] types = asm.GetTypes();
foreach (Type t in types) {
sb.Append(" 类型:" + t + "\n");
}
Console.WriteLine(sb.ToString());
}
然后,我们在Main()方法中调用一下,应该可以看到这样的输出结果:
FullName(全名):Demo, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
Location(路径):E:\MyApp\TypeExplorer\SimpleExplorer\bin\Debug\Demo.dll
模块: Demo.dll
类型:Demo.BaseClass
类型:Demo.DemoStruct
类型:Demo.DemoDelegate
类型:Demo.DemoEnum
类型:Demo.IDemoInterface
类型:Demo.IDemoInterface2
类型:Demo.DemoClass
类型:Demo.DemoClass+NestedClass
反射基本类型
这里说反射基本类型,基本类型是针对 泛型类型 来说的,因为 反射泛型 会更加复杂一些。在前面的范例中,我们获得了程序集中的所有类型,并循环打印了它们,打印结果仅仅显示出了类型的全名,而我们通常需要关于类型更详细的信息,本节我们就来看看如何进一步查看类型信息。
NOTE:因为一个程序集包含很多类型,一个类型包含很多成员(方法、属性等),一个成员又包含很多其他的信息,所以如果我们从程序集层次开始写代码去获取每个层级的信息,那么会嵌套很多的foreach语句,为了阅读方便,我会去掉最外层的循环。
1.获取基本信息
有了前面Type一节的介绍,我想完成这里应该只是打打字而已,所以我直接写出代码,如有必要,会在注释中加以说明。我们再写一个方法TypeExplore,用于获取类型的详细信息(记得AssemblyExplore只获取了类型的名称):
public static void TypeExplore(Type t) {
StringBuilder sb = new StringBuilder();
sb.Append("名称信息:\n");
sb.Append("Name: " + t.Name + "\n");
sb.Append("FullName: " + t.FullName + "\n");
sb.Append("Namespace: " + t.Namespace + "\n");
sb.Append("\n其他信息:\n");
sb.Append("BaseType(基类型): " + t.BaseType + "\n");
sb.Append("UnderlyingSystemType: " + t.UnderlyingSystemType + "\n");
sb.Append("\n类型信息:\n");
sb.Append("Attributes(TypeAttributes位标记): " + t.Attributes + "\n");
sb.Append("IsValueType(值类型): " + t.IsValueType + "\n");
sb.Append("IsEnum(枚举): " + t.IsEnum + "\n");
sb.Append("IsClass(类): " + t.IsClass + "\n");
sb.Append("IsArray(数组): " + t.IsArray + "\n");
sb.Append("IsInterface(接口): " + t.IsInterface + "\n");
sb.Append("IsPointer(指针): " + t.IsPointer + "\n");
sb.Append("IsSealed(密封): " + t.IsSealed + "\n");
sb.Append("IsPrimitive(基类型): " + t.IsPrimitive + "\n");
sb.Append("IsAbstract(抽象): " + t.IsAbstract + "\n");
sb.Append("IsPublic(公开): " + t.IsPublic + "\n");
sb.Append("IsNotPublic(不公开): " + t.IsNotPublic + "\n");
sb.Append("IsVisible: " + t.IsVisible + "\n");
sb.Append("IsByRef(由引用传递): " + t.IsByRef + "\n");
Console.WriteLine(sb.ToString());
}
然后,我们在Main方法中输入:
Type t = typeof(DemoClass);
TypeExplore(t);
会得到这样的输出:
名称信息:
Name: DemoClass
FullName: Demo.DemoClass
Namespace: Demo
其他信息:
BaseType(基类型): Demo.BaseClass
UnderlyingSystemType: Demo.DemoClass
类型信息:
Attributes(TypeAttributes位标记): AutoLayout, AnsiClass, Class, Public, Sealed,
BeforeFieldInit
IsValueType(值类型): False
IsEnum(枚举): False
IsClass(类): True
IsArray(数组): False
IsInterface(接口): False
IsPointer(指针): False
IsSealed(密封): True
IsPrimitive(基类型): False
IsAbstract(抽象): False
IsPublic(公开): True
IsNotPublic(不公开): False
IsVisible: True
IsByRef(由引用传递): False
值得注意的是Attributes属性,它返回一个TypeAttributes位标记,这个标记标识了类型的一些元信息,可以看到我们熟悉的Class、Public、Sealed。相应的,IsClass、IsSealed、IsPublic等属性也返回为True。
2.成员信息 与 MemberInfo 类型
我们先考虑一下对于一个类型Type,可能会包含什么类型,常见的有字段、属性、方法、构造函数、接口、嵌套类型等。MemberInfo 类代表着 Type的成员类型,值得注意的是Type类本身又继承自MemberInfo类,理解起来并不困难,因为一个类型经常也是另一类型的成员。Type类提供 GetMembers()、GetMember()、FindMember()等方法用于获取某个成员类型。
我们再添加一个方法 MemberExplore(),来查看一个类型的所有成员类型。
public static void MemberExplore(Type t) {
StringBuilder sb = new StringBuilder();
MemberInfo[] memberInfo = t.GetMembers();
sb.Append("查看类型 " + t.Name + "的成员信息:\n");
foreach (MemberInfo mi in memberInfo) {
sb.Append("成员:" + mi.ToString().PadRight(40) + " 类型: " + mi.MemberType + "\n");
}
Console.WriteLine(sb.ToString());
}
然后我们在Main方法中调用一下。
MemberExplore(typeof(DemoClass));
产生的输出如下:
查看类型 DemoClass的成员信息:
--------------------------------------------------
成员:Void add_myEvent(Demo.DemoDelegate) 类型: Method
成员:Void remove_myEvent(Demo.DemoDelegate) 类型: Method
成员:System.String get_Name() 类型: Method
成员:Void set_Name(System.String) 类型: Method
成员:Void SayGreeting(System.String) 类型: Method
成员:System.Type GetType() 类型: Method
成员:System.String ToString() 类型: Method
成员:Boolean Equals(System.Object) 类型: Method
成员:Int32 GetHashCode() 类型: Method
成员:Void .ctor() 类型: Constructor
成员:System.String Name 类型: Property
成员:Demo.DemoDelegate myEvent 类型: Event
成员:System.String text 类型: Field
成员:Demo.DemoClass+NestedClass 类型: NestedType
我们使用了GetMembers()方法获取了成员信息的一个数组,然后遍历了数组,打印了成员的名称和类型。如同我们所知道的:Name属性在编译后成为了get_Name()和set_Name()两个独立的方法;myEvent事件的注册(+=)和取消注册(-=)分别成为了add_myEvent()和remove_myEvent方法。同时,我们发现私有(private)字段name 没有被打印出来,另外,基类System.Object的成员GetType()和Equals()也被打印了出来。
有的时候,我们可能不希望查看基类的成员,也可能希望查看私有的成员,此时可以使用GetMembers()的重载方法,传入BindingFlags 位标记参数来完成。BindingFlags位标记对如何获取成员的方式进行控制(也可以控制如何创建对象实例,后面会说明)。对于本例,如果我们想获取所有的公有、私有、静态、实例 成员,那么只需要这样修改GetMembers()方法就可以了。
MemberInfo[] memberInfo = t.GetMembers(
BindingFlags.Public |
BindingFlags.Static |
BindingFlags.NonPublic |
BindingFlags.Instance |
BindingFlags.DeclaredOnly
);
此时的输出如下:
查看类型 DemoClass的成员信息:
--------------------------------------------------
成员:Void add_myEvent(Demo.DemoDelegate) 类型: Method
成员:Void remove_myEvent(Demo.DemoDelegate) 类型: Method
成员:System.String get_Name() 类型: Method
成员:Void set_Name(System.String) 类型: Method
成员:Void SayGreeting(System.String) 类型: Method
成员:Void .ctor() 类型: Constructor
成员:System.String Name 类型: Property
成员:Demo.DemoDelegate myEvent 类型: Event
成员:System.String name 类型: Field
成员:Demo.DemoDelegate myEvent 类型: Field
成员:System.String text 类型: Field
成员:Demo.DemoClass+NestedClass 类型: NestedType
可以看到,继承自基类 System.Object 的方法都被过滤掉了,同时,打印出了私有的 name, myEvent 等字段。
现在如果我们想要获取所有的方法(Method),那么我们可以使用 Type类的FindMembers()方法:
MemberInfo[] memberInfo = t.FindMembers(
MemberTypes.Method, // 说明查找的成员类型为 Method
BindingFlags.Public |
BindingFlags.Static |
BindingFlags.NonPublic |
BindingFlags.Instance |
BindingFlags.DeclaredOnly,
Type.FilterName,
"*"
);
Type.FilterName 返回一个MemberFilter类型的委托,它说明按照方法名称进行过滤,最后一个参数“*”,说明返回所有名称(如果使用“Get*”,则会返回所有以Get开头的方法)。现在的输出如下:
查看类型 DemoClass的成员信息:
--------------------------------------------------
成员:Void add_myEvent(Demo.DemoDelegate) 类型: Method
成员:Void remove_myEvent(Demo.DemoDelegate) 类型: Method
成员:System.String get_Name() 类型: Method
成员:Void set_Name(System.String) 类型: Method
成员:Void SayGreeting(System.String) 类型: Method
MemberInfo 类有两个属性值得注意,一个是DeclaringType,一个是 ReflectedType,返回的都是Type类型。DeclaredType 返回的是声明该成员的类型。比如说,回顾我们之前的一段代码:
MemberInfo[] members = typeof(DemoClass).GetMembers();
它将返回所有的公有成员,包括继承自基类的Equals()等方法,对于Equals()方法来说,它的 DeclaringType 返回的是相当于 typeof(Object) 的类型实例,因为它是在 System.Object中被定义的;而它的ReflectedType 返回的则是相当于 typeof(DemoClass) 类型实例,因为它是通过 DemoClass 的类型实例被获取的。
3.字段信息 与 FieldInfo类型
如同我们之前所说,MemberInfo 是一个基类,它包含的是类型的各种成员都公有的一组信息。实际上,对于字段、属性、方法、事件 等类型成员来说,它们包含的信息显然都是不一样的,所以,.Net 中提供了 FiledInfo 类型来封装字段的信息,它继承自MemberInfo。
如果我们希望获取一个类型的所有字段,可以使用 GetFileds()方法。我们再次添加一个方法FieldExplore():
public static void FieldExplore(Type t) {
StringBuilder sb = new StringBuilder();
FieldInfo[] fields = t.GetFields();
sb.Append("查看类型 " + t.Name + "的字段信息:\n");
sb.Append(String.Empty.PadLeft(50, '-') + "\n");
foreach (FieldInfo fi in fields) {
sb.Append("名称:" + fi.Name + "\n");
sb.Append("类型:" + fi.FieldType + "\n");
sb.Append("属性:" + fi.Attributes + "\n\n");
}
Console.WriteLine(sb.ToString());
}
产生的输出如下:
查看类型 DemoClass的字段信息:
--------------------------------------------------
名称:city
类型:System.String
属性:Public
名称:title
类型:System.String
属性:Public, InitOnly
名称:text
类型:System.String
属性:Public, Static, Literal, HasDefault
值得一提的是fi.FieldType 属性,它返回一个FieldAttributes位标记,这个位标记包含了字段的属性信息。对比我们之前定义的DemoClass类,可以看到,对于title 字段,它的属性是public, InitOnly;对于Const类型的text字段,它的属性为Public,Static,Literal,HasDefault,由此也可以看出,声明一个const类型的变量,它默认就是静态static的,同时,由于我们给了它初始值,所以位标记中也包括HasDefault。
针对于FieldType位标记,FiledInfo 类提供了一组返回为bool类型的属性,来说明字段的信息,常用的有:IsPublic, IsStatic, IsInitOnly, IsLiteral, IsPrivate 等。
如果我们想要获取私有字段信息,依然可以使用重载了的GetFields[]方法,传入BindingFlags参数,和上面的类似,这里就不重复了。
4.属性信息 与 PropertyInfo 类型
和字段类似,也可以通过 GetProperty()方法,获取类型的所有属性信息。
public static void PropertyExplore(Type t) {
StringBuilder sb = new StringBuilder();
sb.Append("查看类型 " + t.Name + "的属性信息:\n");
sb.Append(String.Empty.PadLeft(50, '-') + "\n");
PropertyInfo[] properties = t.GetProperties();
foreach (PropertyInfo pi in properties) {
sb.Append("名称:" + pi.Name + "\n");
sb.Append("类型:" + pi.PropertyType + "\n");
sb.Append("可读:" + pi.CanRead + "\n");
sb.Append("可写:" + pi.CanWrite +"\n");
sb.Append("属性:" + pi.Attributes +"\n");
}
Console.WriteLine(sb.ToString());
}
输出如下:
查看类型 DemoClass的属性信息:
--------------------------------------------------
名称:Name
类型:System.String
可读:True
可写:True
属性:None
从前面的章节可以看到,Name属性会在编译后生成Get_Name()和Set_Name()两个方法,那么,应该可以利用反射获取这两个方法。PropertyInfo类的GetGetMethod()和GetSetMethod()可以完成这个工作,它返回一个MethodInfo对象,封装了关于方法的信息,我们会在后面看到。
5.方法信息 与 MethodInfo 类型
与前面的类似,我们依然可以编写代码来查看类型的方法信息。
public static void MethodExplore(Type t) {
StringBuilder sb = new StringBuilder();
sb.Append("查看类型 " + t.Name + "的方法信息:\n");
sb.Append(String.Empty.PadLeft(50, '-') + "\n");
MethodInfo[] methods = t.GetMethods();
foreach (MethodInfo method in methods) {
sb.Append("名称:" + method.Name +"\n");
sb.Append("签名:" + method.ToString() + "\n");
sb.Append("属性:" + method.Attributes + "\n");
sb.Append("返回值类型:" + method.ReturnType + "\n\n");
}
Console.WriteLine(sb.ToString());
}
与前面类似,MethodInfo 类也有一个Attributes属性,它返回一个MethodAttribute,MethodAttribute 位标记标明了方法的一些属性,常见的比如Abstract, Static, Virtual,Public, Private 等。
与前面不同的是,Method可以具有参数 和 返回值,MethodInfo 类提供了 GetParameters() 方法获取 参数对象的数组,方法的参数都封装在了 ParameterInfo 类型中。查看ParameterInfo类型的方法与前面类似,这里就不再阐述了。
4. ConstructorInfo类型、EventInfo 类型
从名称就可以看出来,这两个类型封装了类型 的构造函数 和 事件信息,大家都是聪明人,查看这些类型与之前的方法类似,这里就不再重复了。
5.小结
本文涉及了反射的最基础的内容,我们可以利用反射来自顶向下地查看程序集、模块、类型、类型成员的信息。反射更强大、也更有意思的内容:迟绑定方法、动态创建类型以后会再讲到。
posted @ 2009-10-19 10:27 大漠银狐 阅读(17) 评论(0) 编辑
.Net 中的反射(序章) - Part.1
转自:http://www.cnblogs.com/JimmyZhang/archive/2008/01/27/Reflection-Part1.html
引言
反射是.Net提供给我们的一件强力武器,尽管大多数情况下我们不常用到反射,尽管我们可能也不需要精通它,但对反射的使用作以初步了解在日后的开发中或许会有所帮助。
反射是一个庞大的话题,牵扯到的知识点也很多,包括程序集、自定义特性、泛型等,想要完全掌握它非常不易。本文仅仅对反射做一个概要介绍,关于它更精深的内容,需要在实践中逐渐掌握。本文将分为下面几个部分介绍.Net中的反射:
- 序章,我将通过一个例子来引出反射,获得对反射的第一印象。
- 反射初步、Type类、反射普通类型。(修改中,近期发布...)
- 反射特性(Attribute)。
- xxxx (待定)
- ...
序章
如果你还没有接触过反射,而我现在就下一堆定义告诉你什么是反射,相信你一定会有当头一棒的感觉。我一直认为那些公理式的定义和概念只有在你充分懂得的时候才能较好的发挥作用。所以,我们先来看一个开发中常遇到的问题,再看看如何利用反射来解决:
在进行数据库设计的过程中,常常会建立一些基础信息表,比如说:全国的城市,又或者订单的状态。假设我们将城市的表,起名为City,它通常包含类似这样的字段:
Id Int Identity(1,1) 城市Id
Name Varchar(50) 城市名称
ZIP Varchar(10) 城市邮编
... // 略
这个表将供许多其他表引用。假如我们在建立一个酒店预订系统,那么酒店信息表(Hotel)就会引用此表,用CityId字段来引用酒店所在城市。对于城市(City)表这种情况,表里存放的记录(城市信息)是不定的,意思就是说:我们可能随时会向这张表里添加新的城市(当某个城市的第一家酒店想要加入预订系统时,就需要在City表里新添这家酒店所在的城市)。此时,这样的设计是合理的。
1.建表及其问题
我们再看看另外一种情况,我们需要标识酒店预订的状态:未提交、已提交、已取消、受理中、已退回、已订妥、已过期。此时,很多开发人员会在数据库中建立一张小表,叫做BookingStatus(预订状态),然后将如上状态加入进去,就好像这样:
如同城市(City)表一样,在系统的其他表,比如说酒店订单表(HotelOrder)中,通过字段StatusId引用这个表来获取酒店预订状态。然而,几个月以后,虽然看上去和城市表的用法一样,结果却发现这个表只在数据库做联合查询或者 只在程序中调用,却从来不做修改,因为预订流程确定下来后一般是不会变更的。在应用程序中,也不会给用户提供对这个表记录的增删改操作界面。
而在程序中调用这个表时,经常是这种情况:我们需要根据预订状态对订单列表进行筛选。此时通常的做法是使用一个下拉菜单(DropDownList),菜单的数据源(DataSource),我们可以很轻易地通过一个SqlDataReader获得,我们将DropDownList的文本Text设为Status字段,将值Value设为Id字段。
此时,我们应该已经发现问题:
- 如果我们还有航班预订、游船预订,或者其他一些状态,我们需要在数据库中创建很多类似的小表,造成数据库表的数目过多。
- 我们使用DropDownList等控件获取表内容时,需要连接到数据库进行查询,潜在地影响性能。
同时,我们也注意到三点:
- 此表一般会在数据库联合查询中使用到。假设我们有代表酒店订单的HotelOrder表,它包含代表状态的StatusId字段,我们的查询可能会像这样:Select *, (Select Status From BookingStatus Where Id = HotelOrder.StatusId) as Status From HotelOrder。
- 在应用程序中,此表经常作为DropDownList或者其他List控件的数据源。
- 这个表几乎从不改动。
2.数组及其问题
意识到这样设计存在问题,我们现在就想办法解决它。我们所想到的第一个办法是可以在程序中创建一个数组来表示预订状态,这样我们就可以删掉BookingStatus状态表(注意可以这样做是因为BookingStatus表的内容确定后几乎从不改动)。
string[] BookingStatus = {
"NoUse", "未提交","已提交","已取消","受理中","已退回","已订妥","已过期"
}; // 注意数组的0号元素仅仅是起一个占位作用,以使程序简洁。因为StatusId从1开始。
我们先看它解决了什么:上面提到的问题1、问题2都解决了,既不需要在数据库中创建表,又无需连接到数据库进行查询。
我们再看看当我们想要用文本显示酒店的预订时,该怎么做(假设有订单类HotelOrder,其属性StatusId代表订单状态,为int类型 )。
// GetItem用于获取一个酒店订单对象, orderId为int类型,代表订单的Id
HotelOrder myOrder = GetItem(orderId);
lbStatus.Text = BookingStatus[myOrder.StatusId]; //lbStatus是一个Label控件
目前为止看上去还不错,现在我们需要进行一个操作,将订单的状态改为“受理中”。
myOrder.StatusId = 4;
很不幸,我们发现了使用数组可能带来的第一个问题:不方便使用,当我们需要更新订单的状态值时,我们需要去查看BookingStatus数组的定义(除非你记住所有状态的数字值),然后根据状态值在数组中的位置来给对象的属性赋值。
我们再看另一个操作,如果某个订单的状态为“已过期”,就要对它进行删除:
if(BookingStatus[myOrder.StatusId]=="已过期"){
DeleteItem(myOrder); // 删除订单
}
此时的问题和上面的类似:我们需要手动输入字符串“已过期”,此时Vs2005 的智能提示发挥不了任何作用,如果我们不幸将状态值记错,或者手误打错,就将导致程序错误,较为稳妥的做法还是按下F12导向到BookingStatus数组的定义,然后将“已过期”复制过来。
现在,我们再看看如何来绑定到一个DropDownList下拉列表控件(Id为ddlStatus)上。
ddlStatus.DataSource = BookingStatus;
ddlStatus.DataBind();
但是我们发现产生的HTML代码是这样:
<select name="ddlStatus" id="ddlStatus">
<option value="未提交">未提交</option>
<option value="已提交">已提交</option>
<option value="已取消">已取消</option>
<option value="受理中">受理中</option>
<option value="已退回">已退回</option>
<option value="已订妥">已订妥</option>
<option value="已过期">已过期</option>
</select>
我们看到,列表项的value值与text值相同,这显然不是我们想要的,怎么办呢?我们可以给下拉列表写一个数据绑定的事件处理方法。
protected void Page_Load(object sender, EventArgs e) {
ddlStatus.DataSource = BookingStatus;
ddlStatus.DataBound += new EventHandler(ddlStatus_DataBound);
ddlStatus.DataBind();
}
void ddlStatus_DataBound(object sender, EventArgs e) {
int i = 0;
ListControl list = (ListControl)sender; //注意,将sender转换成ListControl
foreach (ListItem item in list.Items) {
i++;
item.Value = i.ToString();
}
}
这样,我们使用数组完成了我们期望的效果,虽然这样实现显得有点麻烦,虽然还存在上面提到的不便于使用的问题,但这些问题我们耐心细心一点就能克服,而软件开发几乎从来就没有100%完美的解决方案,那我们干脆就这样好了。
NOTE:在ddlStatus_DataBound事件中,引发事件的对象sender显然是DropDownList,但是这里却没有将sender转换成DropDownList,而是将它转换成基类型ListControl。这样做是为了更好地进行代码重用,ddlStatus_DataBound事件处理方法将不仅限于 DropDownList,对于继承自ListControl的其他控件,比如RadioButtonList、ListBox也可以不加改动地使用ddlStatus_DataBound方法。
如果你对事件绑定还不熟悉,请参考 C#中的委托和事件 一文。
这里也可以使用Dictionary<String, Int>来完成,但都存在类似的问题,就不再举例了。
3.枚举及其问题
然而不幸的事又发生了... 我们的预订程序分为两部分:一部分为B/S端,在B/S端可以进行酒店订单的 创建(未提交)、提交(已提交)、取消提交(已取消),另外还可以看到是不是已订妥;一部分为C/S端,为酒店的预订中心,它可以进行其他状态的操作。
此时,对于整个系统来说,应该有全部的7个状态。但对于B/S端来说,它只有 未提交、已提交、已取消、已订妥 四个状态,对应的值分别为 1、2、3、6。
我们回想一下上面是如何使用数组来解决的,它存在一个缺陷:我们默认地将订单状态值与数组的索引一一对应地联系了起来。
所以在绑定DropDownList时,我们采用自增的方式来设定列表项的Value值;或者在显示状态时,我们通过lbStatus.Text = BookingStatus[myOrder.StatusId]; 这样的语句来完成。而当这种对应关系被打破时,使用数组的方法就失效了,因为如果不利用数组索引,我们没有额外的地方去存储状态的数字值。
此时,我们想到了使用枚举:
public enum BookingStatus {
未提交 = 1,
已提交,
已取消,
已订妥 = 6
}
我们想在页面输出一个订单的状态时,可以这样:
HotelOrder myOrder = GetItem(orderId); //获取一个订单对象
lbStatus.Text = ((BookingStatus)myOrder.StatusId).ToString(); // 输出文本值
我们想更新订单的状态为 “已提交”:
myOrder.StatusId = (int)BookingStatus.已提交;
当状态为“已取消”时我们想执行某个操作:
if(BookingStatus.已取消 == (BookingStatus)myOrder.StatusId){
// Do some action
}
此时,VS 2005 的智能提示已经可以发挥完全作用,当我们在BookingStatus后按下“.”时,可以显示出所有的状态值。
NOTE:当我们使用枚举存储状态时,myOrder对象的StatusId最好为BookingStatus枚举类型,而非int类型,这样操作会更加便捷一些,但为了和前面使用数组时的情况保持统一,这里StatusId仍使用int类型。
以上三种情况使用枚举都显得非常的流畅,直到我们需要绑定枚举到DropDownList下拉列表的时候:我们知道,可以绑定到下拉列表的有两类对象,一类是实现了IEnumerable接口的可枚举集合,比如ArrayList,String[],List<T>;一类是实现了IListSource的数据源,比如DataTable,DataSet。
NOTE:实际上IListSource接口的GetList()方法返回一个IList接口,IList接口又继承了IEnumerable接口。由此看来,IEnumerable是实现可枚举集合的基础,在我翻译的一篇文章 C#中的枚举器 中,对这个主题做了详细的讨论。
可我们都知道:枚举enum是一个基本类型,它不会实现任何的接口,那么我们下来该如何做呢?
4.使用反射遍历枚举字段
最笨也是最简单的办法,我们可以先创建一个GetDataTable方法,此方法依据枚举的字段值和数字值构建一个DataTable,最后返回这个构建好的DataTable:
private static DataTable GetDataTable() {
DataTable table = new DataTable();
table.Columns.Add("Name", Type.GetType("System.String")); //创建列
table.Columns.Add("Value", Type.GetType("System.Int32")); //创建列
DataRow row = table.NewRow();
row[0] = BookingStatus.未提交.ToString();
row[1] = 1;
table.Rows.Add(row);
row = table.NewRow();
row[0] = BookingStatus.已提交.ToString();
row[1] = 2;
table.Rows.Add(row);
row = table.NewRow();
row[0] = BookingStatus.已取消.ToString();
row[1] = 3;
table.Rows.Add(row);
row = table.NewRow();
row[0] = BookingStatus.已订妥.ToString();
row[1] = 6;
table.Rows.Add(row);
return table;
}
接下来,为了方便使用,我们再创建一个专门采用这个DataTable来设置列表控件的方法SetListCountrol():
// 设置列表
public static void SetListControl(ListControl list) {
list.DataSource = GetDataTable(); // 获取DataTable
list.DataTextField = "Name";
list.DataValueField = "Value";
list.DataBind();
}
现在,我们就可以在页面中这样去将枚举绑定到列表控件:
protected void Page_Load(object sender, EventArgs e)
{
SetListControl(ddlStatus); // 假设页面中已有ID为ddlStatus 的DropDownList
}
如果所有的枚举都要通过这样去绑定到列表,我觉得还不如在数据库中直接建表,这样实在是太麻烦了,而且我们是根据枚举的文本和值去HardCoding出一个DataTable的:
DataRow row = table.NewRow();
row[0] = BookingStatus.未提交.ToString();
row[1] = 1;
table.Rows.Add(row);
row = table.NewRow();
row[0] = BookingStatus.已提交.ToString();
row[1] = 2;
table.Rows.Add(row);
row = table.NewRow();
row[0] = BookingStatus.已取消.ToString();
row[1] = 3;
table.Rows.Add(row);
row = table.NewRow();
row[0] = BookingStatus.已订妥.ToString();
row[1] = 6;
table.Rows.Add(row);
这个时候,我们想有没有办法通过遍历来实现这里?如果想要遍历这里,首先,我们需要一个包含枚举的每个字段信息的对象,这个对象至少包含两条信息,一个是字段的文本(比如“未提交”),一个是字段的数字型值(比如1),我们暂且管这个对象叫做field。其次,应该存在一个可遍历的、包含了字段信息的对象(也就是filed) 的集合,我们暂且管这个集合叫做enumFields。
那么,上面就可以这样去实现:
foreach (xxxx field in enumFields)
{
DataRow row = table.NewRow();
row[0] = field.Name; // 杜撰的属性,代表 文本值(比如“未提交”)
row[1] = filed.intValue; // 杜撰的属性,代表 数字值(比如1)
table.Rows.Add(row);
}
这段代码很不完整,我们注意到 xxxx,它应该是封装了字段信息(或者叫元数据metadata)的对象的类型。而对于enumFields,它的类型应该是xxxx这个类型的集合。这段代码是我们按照思路假想和推导出来的。实际上,.Net 中提供了 Type类 和 System.Reflection命名空间来帮助解决我们现在的问题。
我在后面将较详细地介绍 Type类,现在只希望你能对反射有个第一印象,所以只简略地作以说明:Type抽象类提供了访问类型元数据的能力,当你实例化了一个Type对象后,你可以通过它的属性和方法,获取类型的元数据信息,或者进一步获得该类型的成员的元数据。注意到这里,因为Type对象总是基于某一类型的,并且它是一个抽象类,所以我们在创建Type类型时,必须要提供 类型,或者类型的实例,或者类型的字符串值(Part.2会说明)。
创建Type对象有很多种方法,本例中,我们使用typeof操作符来进行,并传递BookingStatus枚举:
Type enumType = typeof(BookingStatus);
然后,我们应该想办法获取 封装了字段信息的对象 的集合。Type类提供 GetFields()方法来实现这一过程,它返回一个 FieldInfo[] 数组。实际上,也就是上面我们enumFields集合的类型。
FieldInfo[] enumFields = enumType.GetFields();
现在,我们就可以遍历这一集合:
foreach (FieldInfo field in enumFields)
{
if (!field.IsSpecialName)
{
DataRow row = table.NewRow();
row[0] = field.Name; // 获取字段文本值
row[1] = Convert.ToInt32(myField.GetRawConstantValue()); // 获取int数值
table.Rows.Add(row);
}
}
这里field的Name属性获取了枚举的文本,GetRawConstantValue()方法获取了它的int类型的值。
我们看一看完整的代码:
private static DataTable GetDataTable() {
Type enumType = typeof(BookingStatus); // 创建类型
FieldInfo[] enumFields = enumType.GetFields(); //获取字段信息对象集合
DataTable table = new DataTable();
table.Columns.Add("Name", Type.GetType("System.String"));
table.Columns.Add("Value", Type.GetType("System.Int32"));
// 遍历集合
foreach (FieldInfo field in enumFields) {
if (!field.IsSpecialName) {
DataRow row = table.NewRow();
row[0] = field.Name;
row[1] = Convert.ToInt32(field.GetRawConstantValue());
//row[1] = (int)Enum.Parse(enumType, field.Name); //也可以这样
table.Rows.Add(row);
}
}
return table;
}
注意,SetListControl()方法依然存在并有效,只是为了节省篇幅,我没有复制过来,它的使用和之前是一样的,我们只是修改了GetDataTable()方法。
5.使用泛型来达到代码重用
观察上面的代码,如果我们现在有另一个枚举,叫做TicketStatus,那么我们要将它绑定到列表,我们唯一需要改动的就是这里:
Type enumType = typeof(BookingStatus); //将BookingStatus改作TicketStatus
既然这样,我们何不定义一个泛型类来进行代码重用呢?我们管这个泛型类叫做EnumManager<TEnum>。
public static class EnumManager<TEnum>
{
private static DataTable GetDataTable()
{
Type enumType = typeof(TEnum); // 获取类型对象
FieldInfo[] enumFields = enumType.GetFields();
DataTable table = new DataTable();
table.Columns.Add("Name", Type.GetType("System.String"));
table.Columns.Add("Value", Type.GetType("System.Int32"));
//遍历集合
foreach (FieldInfo field in enumFields)
{
if (!field.IsSpecialName)
{
DataRow row = table.NewRow();
row[0] = field.Name;
row[1] = Convert.ToInt32(field.GetRawConstantValue());
//row[1] = (int)Enum.Parse(enumType, field.Name); 也可以这样
table.Rows.Add(row);
}
}
return table;
}
public static void SetListControl(ListControl list)
{
list.DataSource = GetDataTable();
list.DataTextField = "Name";
list.DataValueField = "Value";
list.DataBind();
}
}
OK,现在一切都变得简便的多,以后,我们再需要将枚举绑定到列表,只要这样就行了(ddl开头的是DropDownList,rbl开头的是RadioButtonList):
EnumManager<BookingStauts>.SetListControl(ddlBookingStatus);
EnumManager<TicketStatus>.SetListControl(rblTicketStatus);
NOTE:如果你对泛型不熟悉,请参阅 C# 中的泛型 一文。上面的实现并没有考虑到性能的问题,仅仅为了引出反射使用的一个实例。
6 .Net 中反射的一个范例。
不管是VS2005的智能提示,还是修改变量名时的重构功能,都使用了反射功能。在.Net FCL中,也经常能看到反射的影子,这里就向大家演示一个最常见的例子。大家知道,在CLR中一共有两种类型,一种是值类型,一种是引用类型。声明一个引用类型的变量并对类型实例化,会在应用程序堆(Application Heap)上分配内存,创建对象实例,然后将对象实例的内存地址返回给变量,变量保存的是内存地址,实际相当于一个指针;声明一个值类型的实例变量,则会将它分配在线程堆栈(Thread Stack)上,变量本身包含了值类型的所有字段。
现在假设我们需要比较两个对象是否相等。当我们比较两个引用类型的变量是否相等时,我们比较的是这两个变量所指向的是不是堆上的同一个实例(内存地址是否相同)。而当我们比较两个值类型变量是否相等时,怎么做呢?因为变量本身就包含了值类型所有的字段(数据),所以在比较时,就需要对两个变量的字段进行逐个的一对一的比较,看看每个字段的值是否都相等,如果任何一个字段的值不等,就返回false。
实际上,执行这样的一个比较并不需要我们自己编写代码,Microsoft已经为我们提供了实现的方法:所有的值类型继承自 System.ValueType, ValueType和所有的类型都继承自System.Object ,Object提供了一个Equals()方法,用来判断两个对象是否相等。但是ValueType覆盖了Object的Equals()方法。当我们比较两个值类型变量是否相等时,可以调用继承自ValueType类型的Equals()方法。
public struct ValPoint {
public int x;
public int y;
}
static void Main(string[] args) {
bool result;
ValPoint A1;
A1.x = A1.y = 3;
ValPoint B1 = A1; // 复制A的值给B
result = A1.Equals(B1);
Console.WriteLine(result); // 输出 True;
}
你有没有想到当调用Equals()方法时会发生什么事呢?前面我们已经提到如果是值类型,会对两个变量的字段进行逐个的比较,看看每个字段的值是否都相等,但是如何获取变量的所有字段,遍历字段,并逐一比较呢?此时,你应该意识到又到了用到反射的时候了,让我们使用reflector来查看ValueType类的Equals()方法,看看微软是如何做的吧:
public override bool Equals(object obj) {
if (obj == null) {
return false;
}
RuntimeType type = (RuntimeType)base.GetType();
RuntimeType type2 = (RuntimeType)obj.GetType();
if (type2 != type) {
return false;
}
object a = this;
if (CanCompareBits(this)) {
return FastEqualsCheck(a, obj);
}
// 获取所有实体字段
FieldInfo[] fields = type.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
// 遍历字段,判断字段值是否相等
for (int i = 0; i < fields.Length; i++) {
object obj3 = ((RtFieldInfo)fields[i]).InternalGetValue(a, false);
object obj4 = ((RtFieldInfo)fields[i]).InternalGetValue(obj, false);
if (obj3 == null) {
if (obj4 != null) {
return false;
}
} else if (!obj3.Equals(obj4)) {
return false;
}
}
return true;
}
注意到上面加注释的那两段代码,可以看到当对值变量进行比较时,是会使用反射来实现。反射存在着性能不佳的问题(不仅如此,还存在着很多的装箱操作),由此可见,在值类型上调用Equals()方法开销是会很大的。但是这个例子仅仅为了说明反射的用途,我想已经达到了目的。上面的代码不能完全理解也不要紧,后面会再提到。
7.小结
看到这里,你应该对反射有了一个初步的概念(或者叫反射的一个用途):反射是一种宽泛的叫法,它通过 System.Reflection 命名空间 并 配合 System.Type 类,提供了在运行时(Runtime)对于 类型和对象(及其成员)的基本信息 以及 元数据(metadata)的访问能力。
posted @ 2009-10-19 10:16 大漠银狐 阅读(22) 评论(0) 编辑
.Net 中的反射(动态创建类型实例) - Part.4
转自:http://www.cnblogs.com/JimmyZhang/archive/2008/03/18/1110711.html
源码下载:http://www.tracefact.net/SourceCode/Reflection4.rar
动态创建对象
在前面节中,我们先了解了反射,然后利用反射查看了类型信息,并学习了如何创建自定义特性,并利用反射来遍历它。可以说,前面三节,我们学习的都是反射是什么,在接下来的章节中,我们将学习反射可以做什么。在进行更有趣的话题之前,我们先看下如何动态地创建一个对象。
我们新建一个Console控制台项目,叫做Reflection4(因为本文是Part4,你也可以起别的名字)。然后,添加一个示范类,本文中将通过对这个示范类的操作来进行说明:
public class Calculator {
private int x;
private int y;
public Calculator(){
x = 0;
y = 0;
}
public Calculator(int x, int y) {
this.x = x;
this.y = y;
}
}
1.使用无参数构造函数创建对象
上面这个类非常简单,它包含两个构造函数,一个是有参数的构造函数,一个是无参数的构造函数,我们先看看通过反射,使用无参数的构造函数创建对象。创建对象通常有两种方式,一种是使用Assembly的CreateInstance方法:
Assembly asm = Assembly.GetExecutingAssembly();
Object obj = asm.CreateInstance("Reflection4.Calculator", true);
// 输出:Calculator() invoked
CreateInstance的第一个参数代表了要创建的类型实例的字符串名称,第二个参数说明是不是大小写无关(Ignore Case)。注意到CreateInstance返回的是一个Object对象,意味着如果想使用这个对象,需要进行一次类型转换。
创建对象的另一种方式是调用Activator类的静态方法CreateInstance:
ObjectHandle handler = Activator.CreateInstance(null, "Reflection4.Calculator");
Object obj = handler.Unwrap();
其中CreateInstance的第一个参数说明是程序集的名称,为null时表示当前程序集;第二个参数说明要创建的类型名称。Activator.CreateInstance返回的是一个ObjectHandle对象,必须进行一次Unwrap()才能返回Object类型,进而可以强制转换成我们需要的类型(本例中是Calculator)。ObjectHandle包含在System.Runtime.Remoting命名空间中,可见它是Remoting相关的,实际上ObjectHandle类只是一个对原类型进行了一个包装以便进行封送,更多内容可以参见 .Net Remoting(应用程序域)—Part.1 这篇文章。
2.使用有参数构造函数创建对象
如果我们想通过有参数的构造函数创建对象,我们可以使用Assembly的CreateInstanc()的重载方法:
// 有参数构造函数创建对象
Assembly asm = Assembly.GetExecutingAssembly();
Object[] parameters = new Object[2]; // 定义构造函数需要的参数
parameters[0] = 3;
parameters[1] = 5;
Object obj = asm.CreateInstance("Reflection4.Calculator", true, BindingFlags.Default, null, parameters, null, null);
// 输出:Calculator(int x, int y) invoked
我们看一下CreateInstance需要提供的参数:
- 前两个在前一小节已经说明过了;
- BindingFlags在前面我们也用到过,它用于限定对类型成员的搜索。在这里指定Default,意思是不使用BingdingFlags的策略(你可以把它理解成null,但是BindingFlags是值类型,所以不可能为null,必须有一个默认值,而这个Default就是它的默认值);
- 接下来的参数是Binder,它封装了CreateInstance绑定对象(Calculator)的规则,我们几乎永远都会传递null进去,实际上使用的是预定义的DefaultBinder;
- 接下来是一个Object[]数组类型,它包含我们传递进去的参数,有参数的构造函数将会使用这些参数;
- 接下来的参数是一个CultureInfo类型,它包含了关于语言和文化的信息(简单点理解就是什么时候ToString("c")应该显示“¥”,什么时候应该显示“$”)。
动态调用方法
接下来我们看一下如何动态地调用方法。注意,本文讨论的调用不是将上面动态创建好的对象由Object类型转换成Calculator类型再进行方法调用,这和“常规调用”就没有区别了,让我们以.Net Reflection 的方式来进行方法的调用。继续进行之前,我们为Calculator添加两个方法,一个实例方法,一个静态方法:
public int Add(){
int total= 0;
total = x + y;
Console.WriteLine("Invoke Instance Method: ");
Console.WriteLine(String.Format("[Add]: {0} plus {1} equals to {2}", x, y, total));
return total;
}
public static void Add(int x, int y){
int total = x + y;
Console.WriteLine("Invoke Static Method: ");
Console.WriteLine(String.Format("[Add]: {0} plus {1} equals to {2}", x, y, total));
}
调用方法的方式一般有两种:
- 在类型的Type对象上调用InvokeMember()方法,传递想要在其上调用方法的对象(也就是刚才动态创建的Calculator类型实例),并指定BindingFlags为InvokeMethod。根据方法签名,可能还需要传递参数。
- 先通过Type对象的GetMethond()方法,获取想要调用的方法对象,也就是MethodInfo对象,然后在该对象上调用Invoke方法。根据方法签名,可能还需要传递参数。
需要说明的是,使用InvokeMember不限于调用对象的方法,也可以用于获取对象的字段、属性,方式都是类似的,本文只说明最常见的调用方法。
1.使用InvokeMember调用方法
我们先看第一种方法,代码很简单,只需要两行(注意obj在上节已经创建,是Calculator类型的实例):
Type t = typeof(Calculator);
int result = (int)t.InvokeMember("Add", BindingFlags.InvokeMethod, null, obj, null);
Console.WriteLine(String.Format("The result is {0}", result));
输出:
Invoke Instance Method:
[Add]: 3 plus 5 equals to 8
The result is 8
在InvokeMember方法中,第一个参数说明了想要调用的方法名称;第二个参数说明是调用方法(因为InvokeMember的功能非常强大,不光是可以调用方法,还可以获取/设置 属性、字段等。此枚举的详情可参看Part.2或者MSDN);第三个参数是Binder,null说明使用默认的Binder;第四个参数说明是在这个对象上(obj是Calculator类型的实例)进行调用;最后一个参数是数组类型,表示的是方法所接受的参数。
我们在看一下对于静态方法应该如何调用:
Object[] parameters2 = new Object[2];
parameters2[0] = 6;
parameters2[1] = 9;
t.InvokeMember("Add", BindingFlags.InvokeMethod, null, typeof(Calculator), parameters2);
输出:
Invoke Static Method:
[Add]: 6 plus 9 equals to 15
我们和上面对比一下:首先,第四个参数传递的是 typeof(Calculator),不再是一个Calculator实例类型,这很容易理解,因为我们调用的是一个静态方法,它不是基于某个具体的类型实例的,而是基于类型本身;其次,因为我们的静态方法需要提供两个参数,所以我们以数组的形式将这两个参数进行了传递。
2.使用MethodInfo.Invoke调用方法
我们再看下第二种方式,先获得一个MethodInfo实例,然后调用这个实例的Invoke方法,我们看下具体如何做:
Type t = typeof(Calculator);
MethodInfo mi = t.GetMethod("Add", BindingFlags.Instance | BindingFlags.Public);
mi.Invoke(obj, null);
输出:
Invoke Instance Method:
[Add]: 3 plus 5 equals to 8
请按任意键继续. . .
在代码的第二行,我们先使用GetMethod方法获取了一个方法对象MethodInfo,指定BindingFlags为Instance和Public,因为有两个方法都命名为“Add”,所以在这里指定搜索条件是必须的。接着我们使用Invoke()调用了Add方法,第一个参数obj是前面创建的Calculator类型实例,表明在该实例上创建方法;第二个参数为null,说明方法不需要提供参数。
我们再看下如何使用这种方式调用静态方法:
Type t = typeof(Calculator);
Object[] parameters2 = new Object[2];
parameters2[0] = 6;
parameters2[1] = 9;
MethodInfo mi = t.GetMethod("Add", BindingFlags.Static | BindingFlags.Public);
mi.Invoke(null, parameters2);
// mi.Invoke(t, parameters2); 也可以这样
输出:
Invoke Static Method:
[Add]: 6 plus 9 equals to 15
可以看到与上面的大同小异,在GetMethod()方法中,我们指定为搜索BindingFlags.Static,而不是BindingFlags.Public,因为我们要调用的是静态的Add方法。在Invoke()方法中,需要注意的是第一个参数,不能在传递Calculator类型实例,而应该传递Calculator的Type类型或者直接传递null。因为静态方法不是属于某个实例的。
NOTE:通过上面的例子可以看出:使用反射可以达到最大程度上的多态,举个例子,你可以在页面上放置一个DropDownList控件,然后指定它的Items的value为你某个类的方法的名称,然后在SelectedIndexChanged事件中,利用value的值来调用类的方法。而在以前,你只能写一些if else 语句,先判断DropDownList返回的值,根据值再决定调用哪个方法。使用这种方式,编译器在代码运行之前(或者说用户选择了某个选项之前)完全不知道哪个方法将被调用,这也就是常说的 迟绑定(Late Binding)。
Coding4Fun:遍历System.Drawing.Color结构
我们已经讲述了太多的基本方法和理论,现在让我们来做一点有趣的事情:大家知道在Asp.Net中控件的颜色设置,比如说ForeColor, BackColor等,都是一个System.Draw.Color结构类型。在某些情况下我们需要使用自定义的颜色,那么我们会使用类似这样的方式Color.From#7d190d创建一个颜色值。但有时候我们会觉得比较麻烦,因为这个数字太不直观了,我们甚至需要把这个值贴到PhotoShop中看看是什么样的。
这时候,我们可能会想要使用Color结构提供的默认颜色,也就是它的141个静态属性,但是这些值依然是以名称,比如DarkGreen的形式给出的,还是不够直观,如果能把它们以色块的形式输出到页面就好了,这样我们查看起来会方便的多,以后使用也会比较便利。我已经实现了它,可以点击下面的链接查看:
效果预览:http://www.tracefact.net/demo/reflection/color.aspx
基本实现
现在我们来看一下实现过程:
先创建页面Color.aspx(或其他名字),然后在Head里添加些样式控制页面显示,再拖放一个Panel控件进去。样式表需要注意的是#pnColors div部分,它定义了页面上将显示的色块的样式;Id为pnHolder的Panel控件用于装载我们动态生成的div。
<head>
<style type="text/css">
body{font-size:14px;}
h1{font-size:26px;}
#pnColors div{
float:left;width:140px;
padding:7px 0;
text-align:center;
margin:3px;
border:1px solid #aaa;
font-size:11px;
font-family:verdana, arial
}
</style>
</head>
<body>
<h1>Coding4Fun:使用反射遍历System.Drawing.Color结构</h1>
<form id="form1" runat="server">
<asp:Panel ID="pnColors" runat="server"></asp:Panel>
</form>
</body>
NOTE:如果将页面命名为了 Color.aspx,那么需要在代码后置文件中修改类名,比如改成:Reflection_Color,同时页面顶部也需要修改成Inherits="Reflection_Color",不然会出现命名冲突的问题。
下一步的思路是这样的:我们在phColors中添加一系列的div,这些div也就是页面上我们将要显示的色块。我们设置div的文本为 颜色的名称 和 RGB数值,它的背景色我们设为相应的颜色(色块的其他样式,比如宽、边框、宽度已经在head中定义)。我们知道在Asp.Net中,并没有一个Div控件,只有HtmlGenericControl,此时,我们最好定义一个Div让它继承自HtmlGenericControl。
public class Div:HtmlGenericControl
{
private string name;
public Div(Color c)
: base("div") // 调用基类构造函数,创建一个 Div
{
this.name = c.Name; // 颜色名称
// 设置文本
this.InnerHtml = String.Format("{0}<br />RGB({1},{2},{3})", name, c.R, c.G, c.B);
int total = c.R + c.G + c.B;
if (total <= 255) // 如果底色太暗,前景色改为明色调
this.Style.Add("color", "#eee");
// 设置背景颜色
this.Style.Add("background", String.Format("rgb({0},{1},{2})", c.R, c.G, c.B));
}
}
如同我们前面所描述的,这个Div接受一个Color类型作为构造函数的参数,然后在构造函数中,先设置了它的文本为 颜色名称 和 颜色的各个数值(通过Color结构的R, G, B属性获得)。然后设置了div的背景色为相应的RGB颜色。
NOTE:在上面 if(total<=255)那里,可能有的颜色本身就很暗,如果这种情况再使用黑色的前景色那么文字会看不清楚,所以我添加了判断,如果背景太暗,就将前景色调的明亮一点。
OK,现在我们到后置代码中只要做一点点的工作就可以了:
protected void Page_Load(object sender, EventArgs e)
{
List<Div> list = new List<Div>();
Type t = typeof(Color); // 页首已经包含了 using System.Drawing;
// 获取属性
PropertyInfo[] properties = t.GetProperties(BindingFlags.Static | BindingFlags.Public);
Div div;
// 遍历属性
foreach (PropertyInfo p in properties)
{
// 动态获得属性
Color c;
c = (Color)t.InvokeMember(p.Name, BindingFlags.GetProperty, null, typeof(Color), null);
div = new Div(c);
list.Add(div);
}
foreach (Div item in list) {
pnColors.Controls.Add(item);
}
}
上面的代码是很直白的:先创建一个Div列表,用于保存即将创建的色块。然后获取Color类型的Type实例。接着我们使用GetProperties()方法,并指定BindingFlags获取所有的静态公共属性。然后遍历属性,并使用InvokeMember()方法获取了属性值,因为返回的是一个Object类型,所以我们需要把它强制转换成一个Color类型。注意在这里InvokeMember的BindingFlags指定为GetProperty,意为获取属性值。第四个参数为typeof(Color),因为颜色属性(比如DarkGreen)是静态的,不是针对于某个实例的,如果是实例,则需要传递调用此属性的类型实例。最后,我们根据颜色创建div,并将它加入列表,遍历列表并逐一加入到Id为pnColors的Panal控件中。
现在已经OK了,如果打开页面,应该可以看到类似这样的效果:
为列表排序
上面的页面看上去会比较乱,因为列表大致是按颜色名称排序的(Transparnet例外),我们最好可以让列表基于颜色进行排序。关于列表排序,我在 基于业务对象的排序 一文中已经非常详细地进行了讨论,所以这里我仅给出实现过程,而不再进行讲述。这一小节与反射无关,如果你对排序已经非常熟悉,可以跳过。
在页面上添加一个RadioButtonList控件,将AutoPostBack设为true,我们要求可以按名称和颜色值两种方式进行排序:
排序:
<asp:RadioButtonList ID="rblSort" runat="server" AutoPostBack="true" RepeatDirection="Horizontal" RepeatLayout="Flow">
<asp:ListItem Selected="True">Name</asp:ListItem>
<asp:ListItem>Color</asp:ListItem>
</asp:RadioButtonList>
在后置代码中,添加一个枚举作为排序的依据:
public enum SortBy{
Name, // 按名称排序
Color // 暗颜色值排序
}
修改Div类,添加 ColorValue字段,这个字段代表颜色的值,并创建嵌套类型ColorComparer,以及方法GetComparer:
public class Div:HtmlGenericControl
{
private int colorValue;
private string name;
public Div(Color c)
: base("div") // 调用基类构造函数,创建一个 Div
{
this.name = c.Name; // 颜色名称
this.colorValue = // 颜色的色彩值
c.R * 256 * 256 + c.G * 256 + c.B;
// 设置文本
this.InnerHtml = String.Format("{0}<br />RGB({1},{2},{3})", name, c.R, c.G, c.B);
int total = c.R + c.G + c.B;
if (total <= 255) // 如果底色太暗,前景色改为明色调
this.Style.Add("color", "#eee");
// 设置背景颜色
this.Style.Add("background", String.Format("rgb({0},{1},{2})", c.R, c.G, c.B));
}
// 返回一个Comparer()用于排序
public static ColorComparer GetComparer(SortBy sort) {
return new ColorComparer(sort);
}
// 默认以名称排序
public static ColorComparer GetComparer() {
return GetComparer(SortBy.Name);
}
// 嵌套类型,用于排序
public class ColorComparer : IComparer<Div>
{
private SortBy sort;
public ColorComparer(SortBy sort) {
this.sort = sort;
}
// 实现IComparer<T>接口,根据sort判断以何为依据一进排序
public int Compare(Div x, Div y)
{
if (sort == SortBy.Name)
return String.Compare(x.name, y.name);
else
return x.colorValue.CompareTo(y.colorValue);
}
}
}
在Page_Load事件上面,我们添加语句,获取当前的排序依据(枚举):
SortBy sort;
if (!IsPostBack) {
sort = SortBy.Name;
} else {
sort = (SortBy)Enum.Parse(typeof(SortBy), rblSort.SelectedValue);
}
在将列表输出到页面之前,我们调用列表的Sort方法:
list.Sort(Div.GetComparer(sort)); // 对列表进行排序
foreach (Div item in list) {
pnColors.Controls.Add(item);
}
好了,所有工作都完成了,再次打开页面,可以看到类似如下画面,我们可以按照名称或者颜色值来对列表进行排序显示:
总结
本文分三个部分讲述了.Net中反射的一个应用:动态创建对象和调用对象方法(属性、字段)。我们先学习最常见的动态创建对象的两种方式,随后分别讨论了使用Type.InvokeMember()和MethodInfo.Invoke()方法来调用类型的实例方法和静态方法。最后,我们使用反射遍历了System.Drawing.Color结构,并输出了颜色值。
感谢阅读,希望这篇文章能给你带来帮助!
posted @ 2009-10-19 10:09 大漠银狐 阅读(94) 评论(0) 编辑
一缕清风
一朵白云
一片落叶
一个孤单的身影
这一切整个构成了一个完整的画面。
清风不停
却留下了岁月的痕迹
在心中保存。