12.2 反射
本节先介绍System.Type 类,通过这个类可以访问任何给定数据类型的信息。然后简要介绍System.Reflection.Assembly类,它可以用于访问给定程序集的信息,或者把这个程序集加载到程序中。最后把本节的代码和上一节的代码结合起来,完成WhatsNewAttributes示例。
12.2.1 System.Type类
在本书中的许多场合中都使用了Type类,但它只存储类型的引用:
Type t = typeof(double)
我们以前把Type看作一个类,但它实际上是一个抽象的基类。只要实例化了一个Type对象,就实例化了Type的一个派生类。Type有与每种数据类型对应的派生类,但一般情况下派生的类只提供各种Type方法和属性的不同重载,返回对应数据类型的正确数据。一般不增加新的方法或属性。获取指向给定类型的Type引用有3种常用方式:
● 使用C#的typeof运算符,如上所示。这个运算符的参数是类型的名称(不放在引号中)。
● 使用GetType()方法,所有的类都会从System.Object继承这个类。
double d = 10;
Type t = d.GetType();
在一个变量上调用GetType(),而不是把类型的名称作为其参数。但要注意,返回的Type对象仍只与该数据类型相关:它不包含与类型实例相关的任何信息。如果有一个对象引用,但不能确保该对象实际上是哪个类的实例,这个方法也是很有用的。
● 还可以调用Type类的静态方法GetType():
Type t = Type.GetType("System.Double");
Type是许多反射技术的入口。它执行许多方法和属性,这里不可能列出所有的方法和属性,而主要介绍如何使用这个类。注意,可用的属性都是只读的:可以使用Type确定数据的类型,但不能使用它修改该类型!
1. Type的属性
由Type执行的属性可以分为下述3类:
● 有许多属性都可以获取包含与类相关的各种名称的字符串,如表12-1所示。
表 12-1
属 性
|
返 回 值
|
Name
|
数据类型名
|
FullName
|
数据类型的完全限定名(包括命名空间名)
|
Namespace
|
定义数据类型的命名空间名
|
● 属性还可以进一步获取Type对象的引用,这些引用表示相关的类,如表12-2所示。
表 12-2
属 性
|
返回对应的Type引用
|
BaseType
|
这个Type的直接基本类型
|
UnderlyingSystemType
|
这个Type在 .NET 运行库中映射的类型 (某些.NET基类实际上映射由IL识别的特定预定义类型)
|
● 许多Boolean 属性表示这个类型是一个类、还是一个枚举等。这些属性包括IsAbstract、IsArray、IsClass、IsEnum、IsInterface、IsPointer、IsPrimitive(一种预定义的基本数据类型)、IsPublic、IsSealed和IsValueType
例如,使用一个基本数据类型:
Type intType = typeof(int);
Console.WriteLine(intType.IsAbstract); // writes false
Console.WriteLine(intType.IsClass); // writes false
Console.WriteLine(intType.IsEnum); // writes false
Console.WriteLine(intType.IsPrimitive); // writes true
Console.WriteLine(intType.IsValueType); // writes true
或者使用Vector类:
Type intType = typeof(Vector);
Console.WriteLine(intType.IsAbstract); // writes false
Console.WriteLine(intType.IsClass); // writes true
Console.WriteLine(intType.IsEnum); // writes false
Console.WriteLine(intType.IsPrimitive); // writes false
Console.WriteLine(intType.IsValueType); // writes false
也可以获取定义类型的程序集的引用,该引用作为System.Reflection.Assembly类实例的一个引用来返回:
Type t = typeof (Vector);
Assembly containingAssembly = new Assembly(t);
2. 方法
System.Type的大多数方法都用于获取对应数据类型的成员信息:构造函数、属性、方法和事件等。它有许多方法,但它们都有相同的模式。例如,有两个方法可以获取数据类型的方法信息:GetMethod()和GetMethods()。GetMethod()方法返回System.Reflection.MethodInfo对象的一个引用,其中包含一个方法的信息。GetMethods()返回这种引用的一个数组。其区别是GetMethods()返回所有方法的信息,而GetMethod()返回一个方法的信息,其中该方法包含特定的参数列表。这两个方法都有重载方法,该重载方法有一个附加的参数,即BindingFlags枚举值,表示应返回哪些成员,例如,返回公有成员、实例成员和静态成员等。
例如,GetMethods()最简单的一个重载方法不带参数,返回数据类型所有公共方法的信息:
Type t = typeof(double);
MethodInfo [] methods = t.GetMethods();
foreach (MethodInfo nextMethod in methods)
{
// etc.
}
Type的成员方法如表12-3所示遵循同一个模式。
表 12-3
返回的对象类型
|
方法(名称为复数形式的方法返回一个数组)
|
ConstructorInfo
|
GetConstructor(), GetConstructors()
|
EventInfo
|
GetEvent(), GetEvents()
|
FieldInfo
|
GetField(), GetFields()
|
InterfaceInfo
|
GetInterface(), GetInterfaces()
|
MemberInfo
|
GetMember(), GetMembers()
|
MethodInfo
|
GetMethod(), GetMethods()
|
PropertyInfo
|
GetProperty(), GetProperties()
|
GetMember()和GetMembers()方法返回数据类型的一个或所有成员的信息,这些成员可以是构造函数、属性和方法等。最后要注意,可以调用这些成员,其方式是调用Type的InvokeMember()方法,或者调用MethodInfo, PropertyInfo和其他类的Invoke()方法。
12.2.2 TypeView示例
下面用一个短小的示例TypeView来说明Type类的一些功能,这个示例可以列出数据类型的所有成员。本例中主要介绍double型的TypeView用法,也可以修改该样列中的一行代码,使用其他的数据类型。TypeView提供的信息要比在控制台窗口中显示的信息多得多,所以我们将打破常规,在一个消息框中显示这些信息。运行double型的TypeView示例,结果如图12-1所示。
图 12-1
该消息框显示了数据类型的名称、全名和命名空间,以及底层类型和基类的名称。然后迭代该数据类型的所有公有实例成员,显示所声明类型的每个成员、成员的类型(方法、字段等)以及成员的名称。声明类型是实际声明类型成员的类名(换言之,如果在System.Double中定义或重载,该声明类型就是System.Double,如果成员继承了某个基类,该声明类就是相关基类的名称)。
TypeView不会显示方法的签名,因为我们是通过MemberInfo对象获取所有公有实例成员的信息,参数信息不能通过MemberInfo对象来获得。为了获取该信息,需要引用MemberInfo和其他更特殊的对象,即需要分别获取每一个成员类型的信息。
TypeView会显示所有公有实例成员的信息,但对于double来说,仅定义了字段和方法。把TypeView编译为一个控制台应用程序,可以在控制台应用程序中显示消息框。但是,使用消息框就意味着需要引用基类程序集System.Windows.Forms. dll,它包含System.Windows.Forms命名空间中的类,在这个命名空间中,定义了我们需要的MessageBox类。下面列出TypeView的代码。开始时需要添加两条using语句:
using System;
using System.Text;
using System.Windows.Forms;
using System.Reflection;
需要System.Text的原因是我们要使用StringBuilder对象建立在消息框中显示的文本,以及消息框本身的System.Windows.Forms。全部代码都放在类MainClass中,这个类包含两个静态方法和一个静态字段,StringBuilder的一个实例叫作OutputText,用于创建在消息框中显示的文本。Main方法和类的声明如下所示:
class MainClass
{
Static StringBuilder OutputText = new StringBuilder();
static void Main()
{
// modify this line to retrieve details of any
// other data type
Type t = typeof(double);
AnalyzeType(t);
MessageBox.Show(OutputText.ToString(), "Analysis of type "
+ t.Name);
Console.ReadLine();
}
Main()方法首先声明一个Type对象,表示我们选择的数据类型,再调用方法AnalyzeType(),从Type对象中提取信息,并使用该信息建立输出文本。最后在消息框中显示输出。使用MessageBox类是非常直观的:只需调用其静态方法Show(),给它传递两个字符串,分别为消息框中的文本和标题。这些都由AnalyzeType()来完成:
static void AnalyzeType(Type t)
{
AddToOutput("Type Name: " + t.Name);
AddToOutput("Full Name: " + t.FullName);
AddToOutput("Namespace: " + t.Namespace);
Type tBase = t.BaseType;
if (tBase != null)
{
AddToOutput("Base Type:" + tBase.Name);
}
Type tUnderlyingSystem = t.UnderlyingSystemType;
if (tUnderlyingSystem != null)
{
AddToOutput("UnderlyingSystem Type:" + tUnderlyingSystem.Name);
}
AddToOutput("/nPUBLIC MEMBERS:");
MemberInfo [] Members = t.GetMembers();
foreach (MemberInfo NextMember in Members)
{
AddToOutput(NextMember.DeclaringType + " " +
NextMember.MemberType + " " + NextMember.Name);
}
}
执行这个方法,仅需调用Type对象的各种属性,就可以获得我们需要的类型名称的信息,再调用GetMembers()方法,获得一个MemberInfo对象数组,该数组用于显示每个成员的信息。注意这里使用了一个辅助方法AddToOutput(),该方法创建要在消息框中显示的文本:
static void AddToOutput(string Text)
{
OutputText.Append("/n" + Text);
}
使用下面的命令编译TypeView程序集:
csc /reference:System.Windows.Forms.dll TypeView.cs
12.2.3 Assembly类
Assembly类是在System.Reflection命名空间中定义的,它允许访问给定程序集的元数据,它也包含可以加载和执行程序集(假定该程序集是可执行的)的方法。与Type类一样,Assembly类包含非常多的方法和属性,这里不可能逐一论述。下面仅介绍完成示例WhatsNewAttributes所需要的方法和属性。
在使用Assembly实例做一些工作前,需要把相应的程序集加载到运行进程中。为此,可以使用静态成员Assembly.Load()或Assembly.LoadFrom()。这两个方法的区别是Load()的参数是程序集的名称,运行库会在各个位置上搜索该程序集,这些位置包括本地目录和全局程序集高速缓存。而LoadFrom()的参数是程序集的完整路径名,不会在其他位置搜索该程序集:
Assembly assembly1 = Assembly.Load("SomeAssembly");
Assembly assembly2 = Assembly.LoadFrom
(@"C:/My Projects/Software/SomeOtherAssembly");
这两个方法都有许多其他重载,它们提供了其他安全信息。加载了一个程序集后,就可以使用它的各种属性,例如查找它的全名:
string name = assembly1.FullName;
1. 查找在程序集中定义的类型
Assembly类的一个特性是可以获得在相应程序集中定义的所有类型的信息,只要调用Assembly.GetTypes()方法,就可以返回一个包含所有类型信息的System.Type引用数组,然后就可以按照上一节的方式处理这些Type引用了:
Type[] types = theAssembly.GetTypes();
foreach(Type definedType in types)
{
DoSomethingWith(definedType);
}
2. 查找定制特性
用于查找在程序集或类型中定义了什么定制特性的方法取决于与该特性相关的对象类型。如果要确定程序集中有什么定制特性,就需要调用Attribute类的一个静态方法GetCustomAttributes(),给它传递程序集的引用:
Attribute [] definedAttributes =
Attribute.GetCustomAttributes(assembly1);
// assembly1 is an Assembly object
注意:
这是相当重要的。以前您可能想知道,在定义定制特性时,必须为它们编写类,为什么Microsoft没有更简单的语法。答案就在于此。定制特性与对象一样,加载了程序集后,就可以读取这些特性对象,查看它们的属性,并且调用它们的方法。
GetCustomAttributes()在用于获取程序集的特性时,有两个重载方法:如果在调用它时,除了程序集的引用外,没有指定其他参数,该方法就会返回为这个程序集定义的所有定制特性。当然,也可以通过指定第二个参数来调用它,第二个参数表示特性类的一个Type对象,在这种情况下,GetCustomAttributes()就返回一个数组,该数组包含该特性类的所有特性。
注意,所有的特性都作为一般的Attribute引用来获取。如果要调用为定制特性定义的任何方法或属性,就需要把这些引用显式转换为相关的定制特性类。调用Assembly.GetCustomAttributes()的另一个重载方法,可以获得与给定数据类型相关的定制特性信息,这次传递的是一个Type引用,它描述了要获取的任何相关特性的类型。另一方面,如果要获得与方法、构造函数和字段等相关的特性,就需要调用GetCustomAttributes()方法,该方法是类MethodInfo、 ConstructorInfo和 FieldInfo等的一个成员。
如果只需要给定类型的一个特性,就可以调用GetCustomAttribute()方法,它返回一个Attribute对象。在WhatsNewAttributes示例中使用GetCustomAttribute()方法,是为了确定程序集中是否有特性SupportsWhatsNew。为此,调用GetCustomAttributes(),传递对WhatsNew Attributes程序集的一个引用和SupportWhatsNewAttribute特性的类型。如果有这个特性,就返回一个Attribute实例。如果在程序集中没有定义任何实例,就返回null。如果找到两个或多个实例,GetCustomAttribute()方法就抛出一个异常System.Reflection. AmbiguousMatchException:
Attribute supportsAttribute =
Attribute.GetCustomAttributes(assembly1,
typeof(SupportsWhats NewAttribute));
12.2.4 完成WhatsNewAttributes示例
现在已经有足够的知识来完成WhatsNewAttributes示例了。为该示例中的最后一个程序集LookUpWhatsNew编写源代码,这部分应用程序是一个控制台应用程序,它需要引用其他两个程序集WhatsNewAttributes和VectorClass。这是一个命令行应用程序,但仍可以象前面的TypeView示例那样在消息框中显示结果,因为结果是许多文本,所以不能显示在一个控制台窗口屏幕上。
这个文件的名称为LookUpWhatsNew.cs,编译它的命令是:
csc /reference:WhatsNewAttributes.dll /reference:VectorClass.dll LookUpWhatsNew.cs
在这个文件的源代码中,首先指定要使用的命名空间System.Text,因为需要使用一个StringBuilder对象:
using System;
using System.Reflection;
using System.Windows.Forms;
using System.Text;
using Wrox.ProCSharp.VectorClass;
using Wrox.ProCSharp.WhatsNewAttributes;
namespace Wrox.ProCSharp.LookUpWhatsNew
{
类WhatsNewChecker包含主程序入口和其他方法。我们定义的所有方法都在这个类中,它还有两个静态字段:outputText和backDateTo。outputText包含在准备阶段创建的文本,这个文本要写到消息框中,backDateTo存储了选择的日期——自从该日期以来的所有修改都要显示出来。一般情况下,需要显示一个对话框,让用户选择这个日期,但我们不想编写这段代码,以免转移读者的注意力。因此,把backDateTo硬编码为日期2007年2月1日。在下载这段代码时,很容易修改这个日期:
class WhatsNewChecker
{
static StringBuilder outputText = new StringBuilder(1000);
static DateTime backDateTo = new DateTime(2007, 2, 1);
static void Main()
{
Assembly theAssembly = Assembly.Load("VectorClass");
Attribute supportsAttribute =
Attribute.GetCustomAttribute(
theAssembly, typeof(SupportsWhatsNewAttribute));
string Name = theAssembly.FullName;
AddToMessage("Assembly: " + Name);
if (supportsAttribute == null)
{
AddToMessage( "This assembly does not support WhatsNew attributes");
return;
}
else
AddToMessage("Defined Types:");
Type[] types = theAssembly.GetTypes();
foreach(Type definedType in types)
DisplayTypeInfo(theAssembly, definedType);
MessageBox.Show(outputText.ToString(),
"What/'s New since " + backDateTo.ToLongDateString());
Console.ReadLine();
}
Main()方法首先加载VectorClass程序集,验证它是否真的用SupportsWhatsNew特性来标记。我们知道,VectorClass应用了SupportsWhatsNew特性,虽然才编译了该程序集,但进行这种检查还是必要的,因为用户可能希望检查这个程序集。
验证了这个程序集后,使用Assembly.GetTypes()方法获得一个数组,其中包括在该程序集中定义的所有类型,然后在这个数组中迭代。对每种类型调用一个方法DisplayTypeInfo(),给outputText字段添加相关的文本,包括LastModifiedAttribute实例的信息。最后,显示带有完整文本的消息框。DisplayTypeInfo()方法如下所示:
static void DisplayTypeInfo(Assembly theAssembly, Type type)
{
// make sure we only pick out classes
if (!(type.IsClass))
{
return;
}
AddToMessage("/nclass " + type.Name);
Attribute [] attribs = Attribute.GetCustomAttributes(type);
if (attribs.Length == 0)
{
AddToMessage("No changes to this class/n");
}
else
{
foreach (Attribute attrib in attribs)
{
WriteAttributeInfo(attrib);
}
}
MethodInfo [] methods = type.GetMethods();
AddToMessage("CHANGES TO METHODS OF THIS CLASS:");
foreach (MethodInfo nextMethod in methods)
{
object [] attribs2 =
nextMethod.GetCustomAttributes(
typeof(LastModifiedAttribute), false);
if (attribs != null)
{
AddToMessage(
nextMethod.ReturnType + " " + nextMethod.Name + "()");
foreach (Attribute nextAttrib in attribs2)
{
WriteAttributeInfo(nextAttrib);
}
}
}
}
注意,在这个方法中,首先应检查参数Type引用是否表示一个类。为了简化代码,指定LastModified特性只能应用于类或成员方法,如果该引用不是类(它可能是一个结构、委托或枚举),进行任何处理都是浪费时间。
接着使用Attribute.GetCustomAttributes()方法确定这个类是否有相关的LastModified Attribute实例。如果有,就使用帮助方法WriteAttributeInfo()把它们的信息添加到输出文本中。
最后使用Type.GetMethods()方法迭代这个数据类型的所有成员方法,然后对类的每个方法进行相同的处理:检查每个方法是否有相关的LastModifiedAttribute实例,如果有,用WriteAttributeInfo()显示方法它们。
下面的代码显示了WriteAttributeInfo()方法,它负责确定为给定的LastModifiedAttribute实例显示什么文本,注意这个方法的参数是一个Attribute引用,所以需要先把该引用转换为LastModifiedAttribute引用。之后,就可以使用最初为这个特性定义的属性获取其参数。在把该特性添加到要显示的文本中之前,应检查特性的日期是否是最近的:
static void WriteAttributeInfo(Attribute attrib)
{
LastModifiedAttribute lastModifiedAttrib =
attrib as LastModifiedAttribute;
if (lastModifiedAttrib == null)
{
return;
}
// check that date is in range
DateTime modifiedDate = lastModifiedAttrib.DateModified;
if (modifiedDate < backDateTo)
{
return;
}
AddToMessage(" MODIFIED: " +
modifiedDate.ToLongDateString() + ":");
AddToMessage(" " + lastModifiedAttrib.Changes);
if (lastModifiedAttrib.Issues != null)
{
AddToMessage(" Outstanding issues:" +
lastModifiedAttrib.Issues);
lastModifiedAttrib.Issues);
}
}
最后,是辅助方法AddToMessage():
static void AddToMessage(string message)
{
outputText.Append("/n" + message);
}
}
}
运行这段代码,得到如图12-2所示的结果。
图 12-2
注意,在列出VectorClass程序集中定义的类型时,实际上选择了两个类:Vector和内嵌的VectorEnumerator类。还要注意,这段代码把backDateTo日期硬编码为2月1日,实际上选择的是日期为2月14日的特性(添加集合支持的时间),而不是1月14日(添加IFormattable接口的时间)。