定制特性

反射是一个普通术语,描述了在运行过程中检查和处理程序元素的功能。例如,反射允许完成以下任务:

●       枚举类型的成员

●       实例化新对象

●       执行对象的成员

●       查找类型的信息

●       查找程序集的信息

●       检查应用于类型的定制特性

●       创建和编译新程序集

这个列表列出了许多功能,包括.NET Framework类库提供的一些最强大、最复杂的功能。但本章不可能介绍反射的所有功能,仅讨论最常用的功能。

首先讨论定制特性,定制特性允许把定制的元数据与程序元素关联起来。这些元数据是在编译过程中创建的,并嵌入到程序集中。接着就可以在运行期间使用反射的一些功能检查这些元数据了。

在介绍了定制特性后,本章将探讨支持反射的一些基类,包括System.Type和System. Reflection.Assembly类,它们可以访问反射提供的许多功能。

为了演示定制特性和反射,我们将开发一个示例,说明公司如何定期升级软件,自动解释升级的信息。在这个示例中,要定义几个定制特性,表示程序元素最后修改或创建的日期,以及发生了什么变化。然后使用反射开发一个应用程序,在程序集中查找这些特性,自动显示软件自某个给定日期以来升级的所有信息。

本章要讨论的另一个示例是一个应用程序,该程序读写数据库,并使用定制特性,把类和特性标记为对应的数据库表和列。然后在运行期间从程序集中读取这些特性,使程序可以自动从数据库的相应位置检索或写入数据,无需为每个表或列编写特定的逻辑。

12.1  定制特性

前面介绍了如何在程序的各个数据项上定义特性。这些特性都是Microsoft定义好的,作为.NET Framework类库的一部分,许多特性都得到了C#编译器的支持。对于这些特性,编译器可以以特殊的方式定制编译过程,例如,可以根据StructLayout特性中的信息在内存中布置结构。

.NET Framework也允许用户定义自己的特性。显然,这些特性不会影响编译过程,因为编译器不能识别它们,但这些特性在应用于程序元素时,可以在编译好的程序集中用作元数据。

这些元数据在文档说明中非常有用。但是,使定制特性非常强大的因素是使用反射,代码可以读取这些元数据,使用它们在运行期间作出决策,也就是说,定制特性可以直接影响代码运行的方式。例如,定制特性可以用于支持对定制许可类进行声明代码访问安全检查,把信息与程序元素关联起来,由测试工具使用,或者在开发可扩展的架构时,允许加载插件或模块。

12.1.1  编写定制特性

为了理解编写定制特性的方式,应了解一下在编译器遇到代码中某个应用了定制特性的元素时,该如何处理。以数据库为例,假定有一个C#属性声明,如下所示。

[FieldName("SocialSecurityNumber")]

public string SocialSecurityNumber

{

   get {

      // etc.

当C#编译器发现这个属性有一个特性FieldName时,首先会把字符串Attribute添加到这个名称的后面,形成一个组合名称FieldNameAttribute,然后在其搜索路径的所有命名空间(即在using语句中提及的命名空间)中搜索有指定名称的类。但要注意,如果用一个特性标记数据项,而该特性的名称以字符串Attribute结尾,编译器就不会把该字符串加到组合名称中,而是不修改该特性名。因此,上面的代码实际上等价于:

[FieldNameAttribute("SocialSecurityNumber")]

public string SocialSecurityNumber

{

   get {

   // etc.

编译器会找到含有该名称的类,且这个类直接或间接派生自System.Attribute。编译器还认为这个类包含控制特性用法的信息。特别是属性类需要指定:

●       特性可以应用到哪些程序元素上(类、结构、属性和方法等)

●       它是否可以多次应用到同一个程序元素上

●       特性在应用到类或接口上时,是否由派生类和接口继承

●       这个特性有哪些必选和可选参数

如果编译器找不到对应的特性类,或者找到一个这样的特性类,但使用特性的方式与特性类中的信息不匹配,编译器就会产生一个编译错误。例如,如果特性类指定该特性只能应用于字段,但我们把它应用到结构定义上,就会产生一个编译错误。

继续上面的示例,假定定义了一个FieldName特性:

[AttributeUsage(AttributeTargets.Property,

   AllowMultiple=false,

   Inherited=false)]

public class FieldNameAttribute : Attribute

{

   private string name;

   public FieldNameAttribute(string name)

   {

      this.name = name;

   }

}

下面几节讨论这个定义中的每个元素。

1. AttributeUsage特性

要注意的第一个问题是特性(attribute)类本身用一个特性System.AttributeUsage来标记。这是Microsoft定义的一个特性,C#编译器为它提供了特殊的支持(AttributeUsage根本不是一个特性,它更像一个元特性,因为它只能应用到其他特性上,不能应用到类上)。AttributeUsage主要用于表示定制特性可以应用到哪些类型的程序元素上。这些信息由它的第一个参数给出,该参数是必选的,其类型是枚举类型AttributeTargets。在上面的示例中,指定FieldName特性只能应用到属性(property)上—— 这是因为我们在前面的代码段中把它应用到属性上。AttributeTargets枚举的成员如下:

●       All

●       Assembly

●       Class

●       Constructor

●       Delegate

●       Enum

●       Event

●       Field

●       GenericParameter(仅.NET 2.0提供)

●       Interface

●       Method

●       Module

●       Parameter

●       Property

●       ReturnValue

●       Struct

这个列表列出了可以应用该特性的所有程序元素。注意在把特性应用到程序元素上时,应把特性放在元素前面的方括号中。但是,在上面的列表中,有两个值不对应于任何程序元素:Assembly和Module。特性可以作为一个整体应用到程序集或模块中,而不是应用到代码中的一个元素上,在这种情况下,这个特性可以放在源代码的任何地方,但需要用关键字assembly或module来做前缀:

[assembly: SomeAssemblyAttribute(Parameters)]

[module: SomeAssemblyAttribute(Parameters)]

在指定定制特性的有效目标元素时,可以使用按位OR运算符把这些值组合起来。例如,如果指定FieldName特性可以应用到属性和字段上,可以编写下面的代码:

   [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field,

      AllowMultiple=false,

      Inherited=false)]

   public class FieldNameAttribute : Attribute

也可以使用AttributeTargets.All指定特性可以应用到所有类型的程序元素上。Attributes Usage特性还包含另外两个参数AllowMultiple 和 Inherited。它们用不同的语法来指定:<AttributeName>=<AttributeValue>,而不是只给出这些参数的值。这些参数是可选的,如果需要,可以忽略它们。

AllowMultiple参数表示一个特性是否可以多次应用到同一项上,这里把它设置为false,表示如果编译器遇到下述代码,就会产生一个错误:

[FieldName("SocialSecurityNumber")]

[FieldName("NationalInsuranceNumber")]

public string SocialSecurityNumber

{

   // etc.

如果Inherited参数设置为true,就表示应用到类或接口上的特性也可以自动应用到所有派生的类或接口上。如果特性应用到方法或属性上,也可以自动应用到该方法或属性的重载上。

2. 指定特性参数

下面介绍如何指定定制特性的参数。在编译器遇到下述语句时:

[FieldName("SocialSecurityNumber")]

public string SocialSecurityNumber

{

   // etc.

会检查传送给特性的参数(在本例中,是一个字符串),并查找该特性中带这些参数的构造函数。如果找到一个这样的构造函数,编译器就会把指定的元数据传送给程序集。如果找不到,就生成一个编译错误。如后面所述,反射会从程序集中读取元数据,并实例化它们表示的特性类。因此,编译器需要确保存在这样的构造函数,才能在运行期间实例化指定的特性。

在本例中,仅为FieldNameAttribute提供了一个构造函数,而这个构造函数有一个字符串参数。因此,在把FieldNameAttribute特性应用到一个属性上时,必须为它提供一个字符串参数,如上面的代码所示。

如果可以选择特性的参数类型,当然可以提供构造函数的不同重载方法,但一般是仅提供一个构造函数,使用属性来定义其他可选参数,下面将介绍可选参数。

3. 指定特性的可选参数

在AttributeUsage特性中,可以使用另一个语法,把可选参数添加到特性中。这个语法指定可选参数的名称和值,处理特性类中的公共属性或字段。例如,假定修改SocialSecurityNumber属性的定义,如下所示:

[FieldName("SocialSecurityNumber", Comment="This is the primary key field")]

public string SocialSecurityNumber

{

   // etc.

在本例中,编译器识别第二个参数的语法<ParameterName>=<ParameterValue>,所以不会把这个参数传递给FieldNameAttribute构造函数,而是查找一个有该名称的公用属性或字段(最好不要使用公用字段,所以一般情况下要使用属性),编译器可以用这个属性设置第二个参数的值。如果希望上面的代码工作,必须给FieldNameAttribute添加一些代码:

   [AttributeUsage(AttributeTargets.Property,

      AllowMultiple=false,

      Inherited=false)]

   public class FieldNameAttribute : Attribute

   {

      private string comment;

      public string Comment

      {

         get

         {

            return comment;

         }

         set

         {

            comment = value;

         }

       }

         // etc.

12.1.2  定制特性示例:WhatsNewAttributes

本节开始编写前面描述过的示例WhatsNewAttributes,该示例提供了一个特性,表示最后一次修改程序元素的时间。这个示例比前面所有的示例都复杂,因为它包含3个不同的程序集:

●       WhatsNewAttributes程序集,它包含特性的定义。

●       VectorClass程序集,包含所应用的特性的代码。

●       LookUpWhatsNew程序集,包含显示已改变的数据项信息的项目。

当然,只有LookUpWhatsNew是前面使用的一个控制台应用程序,其余两个程序集都是库文件,它们都包含类的定义,但都没有程序的入口。对于VectorClass程序集,我们使用了VectorAsCollection示例,但删除了入口和测试代码类,只剩下Vector类。

在命令行上编译,以此管理3个相关的程序集要求较高的技巧,所以我们分别给出编译这3个源文件的命令。也可以编辑代码示例,(可以从Wrox Press网站上下载),组合为一个Visual Studio 2005解决方案,详见第14章。下载的文件包含所需的Visual Studio 2005解决方案文件。

1. WhatsNewAttributes 库程序集

首先从核心的WhatsNewAttributes程序集开始。其源代码包含在文件WhatsNewAttri butes.cs中,该文件位于本章示例代码的WhatsNewAttributes解决方案的WhatsNewAttributes项目中。编译为库的语法非常简单:在命令行上,给编译器提供标记target:library即可。要编译WhatsNewAttributes,键入:

csc /target:library WhatsNewAttributes.cs

WhatsNewAttributes.cs文件定义了两个特性类LastModifiedAttribute和SupportsWhats New-Attribute。LastModifiedAttribute特性可以用于标记最后一次修改数据项的时间,它有两个必选参数(该参数传递给构造函数);修改的日期和包含描述修改的字符串。它还有一个可选参数Issues (表示存在一个公共属性),它可以描述该数据项的任何重要问题。

在现实生活中,或许想把特性应用到任何对象上。为了使代码比较简单,这里仅允许将它应用于类和方法,并允许它多次应用到同一项上(AllowMultiple=true),因为可以多次修改一个项,每次修改都需要用一个不同的特性实例来标记。

SupportsWhatsNew是一个较小的类,表示不带任何参数的特性。这个特性是一个程序集的特性,用于把程序集标记为通过LastModifiedAttribute维护的文档说明书。这样,以后查看这个程序集的程序会知道,它读取的程序集是我们使用自动文档说明过程生成的那个程序集。这部分示例的完整源代码如下所示:

using System;

namespace Wrox.ProCSharp.WhatsNewAttributes

{

   [AttributeUsage(

      AttributeTargets.Class | AttributeTargets.Method,

      AllowMultiple=true, Inherited=false)]

   public class LastModifiedAttribute : Attribute

   {

      private DateTime dateModified;

      private string changes;

      private string issues;

      public LastModifiedAttribute(string dateModified, string changes)

      {

         this.dateModified = DateTime.Parse(dateModified);

         this.changes = changes;

      }

      public DateTime DateModified

      {

         get

         {

            return dateModified;

         }

      }

      public string Changes

      {

         get

         {

            return changes;

         }

      }

      public string Issues

      {

         get

         {

            return issues;

         }

         set

         {

            issues = value;

         }

      }

   }

   [AttributeUsage(AttributeTargets.Assembly)]

   public class SupportsWhatsNewAttribute : Attribute

   {

   }

}

从上面的描述可以看出,上面的代码非常简单。但要注意,不必将set访问器提供给Changes和DateModified属性,不需要这些访问器是因为在构造函数中,这些参数都是必选参数。需要get访问器,是因为以后可以读取这些特性的值。

2. VectorClass程序集

本节就使用这些特性,我们用前面的VectorAsCollection 示例的修订版本来说明。注意这里需要引用刚才创建的WhatsNewAttributes库,还需要使用using语句指定相应的命名空间,这样编译器才能识别出这些特性:

using System;

using System.Collections;

using System.Text;

using Wrox.ProCSharp.WhatsNewAttributes;

[assembly: SupportsWhatsNew]

在这段代码中,添加了一行用SupportsWhatsNew特性标记程序集本身的代码。

下面考虑Vector类的代码。我们并不是真的要修改这个类中的任何内容,只是添加两个LastModified特性,以标记出本章对Vector类进行的操作。把Vector定义为一个类,而不是结构,以简化后面显示特性所编写的代码(在VectorAsCollection示例中,Vector是一个结构,但其枚举器是一个类。于是,这个示例的下一个版本在查看程序集时,必须同时考虑类和结构。这会使例子比较复杂)。

namespace Wrox.ProCSharp.VectorClass

{

   [LastModified("14 Feb 2007", "IEnumerable interface implemented " +

       "So Vector can now be treated as a collection")]

   [LastModified("10 Feb 2007", "IFormattable interface implemented " +

       "So Vector now responds to format specifiers N and VE")]

   class Vector : IFormattable, IEnumerable

   {

      public double x, y, z;

      public Vector(double x, double y, double z)

      {

         this.x = x;

         this.y = y;

         this.z = z;

      }

      [LastModified("10 Feb 2002",

                    "Method added in order to provide formatting support")]

      public string ToString(string format, IFormatProvider formatProvider)

      {

         if (format == null)

         {

            return ToString();

         }

再把包含的VectorEnumerator类标记为new:

   [LastModified("14 Feb 2007",

                 "Class created as part of collection support for Vector")]

   private class VectorEnumerator : IEnumerator

   {

为了在命令行上编译这段代码,应键入下面的命令:

csc /target:library /reference:WhatsNewAttributes.dll VectorClass.cs

上面是这个示例的代码。目前还不能运行它,因为我们只有两个库。在描述了反射的工作原理后,就介绍这个示例的最后一部分,查找和显示这些特性。 

相关文章
相关标签/搜索