24.1 元数据和反射
大多数程序都是用来处理数据的。它们读、写、操作和显示数据。(图形也是一种数据的形式。)程序员为某种目的创建和使用一些类型,因此,在设计时必须理解所使用的类型的特性。
然而,对于某些程序来说,它们操作的数据不是数字、文本或图形,而是程序和程序类型本身的信息。
- 有关程序及其类型的数据被称为元数据(metadata),它们保存在程序的程序集中。
- 程序在运行时,可以查看其他程序集或其本身的元数据。一个运行的程序查看本身的元数据或其他程序的元数据的行为叫做反射(reflection)
对象浏览器是显示元数据的程序的一个示例。它可以读取程序集,然后显示所包含的类型以及类型的所有特性和成员。
说明:要使用反射,我们必须使用System.Reflection命名空间 。
24.2 Type类
BCL声明了一个叫做Type的抽象类,它被设计用来包含类型的特性。使用这个为在的对象能让我们获取程序使用的类型的信息。
由于Type是抽象类,因此它不能有实例。而是在运行时,CLR创建从Type(RuntimeType)派生的类的实例,Type包含了类型信息。当我们要访问这些实例时,CLR不会返回派生类的引用而是Type基类的引用。
需要了解的有关Type的重要事项如下:
- 对于程序中用到的每一个类型,CLR都会创建一个包含这个类型信息的Type类型的对象。
- 程序中用到的每一个类型都会关联到独立的Type类的对象。
- 不管创建的类型有多少个实例,只有一个Type对象会关联到这些实例。
我们可以从Type对象中获取需要了解的有关类型的几乎所有信息。
System.Type类精选的成员 成员 成员类型 描述 Name 属性 返回类型的名字 NameSpace 属性 返回包含类型声明的命名空间 GetFields 方法 返回类型的字段列表 GetProperties 方法 返回类型的属性列表 GetMethods 方法 返回类型的方法列表
24.3 获取Type对象
有好几种方式可以获取Type对象,我们将学习使用GetType和typeof运算符来获取Type对象。
object类型包含了一个叫做GetType的方法,它返回对实例的Type对象的引用。由于每一个类型最终都是从object继承的,所以我们可以在任何类型对象上使用GetType方法来获取它的Type对象。如下所示:
Type t = myInstance.GetType();
下面的代码演示了如何声明一个基类以及从它派生的子类。Main方法创建了每一个类的实例并且把这些引用放在了一个叫做bca的数组中以方便使用。在外层的foreach循环中,代码得到了Type对象并且输出类的名字,然后获取类的字段并输出。
class BaseClass { public int BaseField = 0;} class DerivedClass : BaseClass { public int DerivedField = 0;} class Program { static void Main() { var bc = new BaseClass(); var dc = new DerivedClass(); BaseClass[] bca = new BaseClass[] { bc, dc }; foreach (var v in bca) { Type t = v.GetType(); Console.WriteLine("Object type :{0}",t.Name); FieldInfo[] fi = t.GetFields(); foreach (var f in fi) Console.WriteLine(" Field:{0}",f.Name); Console.WriteLine(); } } }
我们还可以使用typeof运算符来获取Type对象符来获取Type对象。只需要提供类型名作为操作数,它就会返回Type对象的引用,如下所示:
Type t = typeof(DerivedClass);
下面的代码给出了一个使用typeof运算符的简单示例:
using System; using System.Reflection; namespace Timers { class BaseClass { public int BaseField = 0;} class DerivedClass : BaseClass { public int DerivedField = 0;} class Program { static void Main() { Type tbc = typeof(DerivedClass); Console.WriteLine("Result is {0}.",tbc.Name); Console.WriteLine("It has the following fields:"); FieldInfo[] fi = tbc.GetFields(); foreach(var f in fi) Console.WriteLine(" {0}",f.Name); } } }
24.4 什么是特性
特性(attribute)是一种允许我们向程序的程序集增加元数据的语言结构。它是用于保存程序结构信息的某种特殊类型的类。
- 将应用了特性的程序结构(program construct)叫做目标(target)。
- 设计用来获取和使用元数据的程序(比如对象浏览器)被叫做特性的消费者(consumer)。
- .NET预定了很多我,我们也可以声明自定义特性。
下图是使用特性中相关组件的概览,并且也演示了如下有关特性的要点:
- 我们在源代码中将特性应用于程序结构。
- 编译器获取源代码并且从特性产生元数据,然后把元数据放到程序集中。
- 消费者程序可以获取特性的元数据以及程序中其他组件的元数据。注意,编译器同时生产和消费特性。
根据惯例,特性名使用Pascal命名法并且以Attribute后缀结尾。当为目标应用特性时,我们可以不使用后缀。例如,对于SerializableAttribute和MyAttributeAttribute这两个特性,我们在把它们应用到结构时可以使用Serializable和MyAttribute短名称。
24.5 应用特性
特性的目的是告诉编译器把程序结构的某组元数据嵌入程序集。我们可以通过把特性应用到结构来实现。
- 在结构前放置特性片段来应用特性。
- 特性片段被方括号包围,其中是特性名和特性的参数列表。
例如,下面的代码演示了两个类的开始部分。最初的几行代码演示了把一个叫做Serializable的特性应用到MyClass。注意,Serializable没有参数列表。第二个类的声明有一个叫做MyAttribute的特性,它有一个带有两个string参数的参数列表。
[Serializable]public class MyClass{} //特性
[MyAttribute("Simple class","Version 3.57")] //带有参数的特性public class MyOtherClass{}
一些有关特性的需要了解的重要事项如下:
- 大多数特性只针对直接跟随在一个或多个特性片段后的结构。
- 应用了特性的结构称作被特性装饰(decorated或adorned,两者没有什么区别)。
24.6 预定义的保留的特性
在学习如何定义自己的特性之前,本小节会先介绍.NET的两个预定义的、保留的特性:Obsolete和Conditional特性。
Obsolete特性
Obsolete特性允许我们将程序结构标为过期的并且在代码编译时显示有用的警告消息。以下代码给出了一个使用的示例。
class Class4 { [Obsolete("Use method SuperPrintOut")] //将特性应用到方法 static void PrintOut(string str) { Console.WriteLine(str); } static void Main(string[] args) { PrintOut("Start of Main"); } }
注意,即使PrintOut被标注为过期,Main方法还是调用了它。不过,在编译的过程中,编译器产生了下面的CS0618警告消息来通知我们正在使用一个过期的结构。另外一个Obsolete特性的重载接受了bool类型的第二个参数。这个参数指定目标是否应该被标记为错误而不仅仅是警告。以下代码指定了它需要被标记为错误:
[Obsolete("Use method SuperPrintOut",true)] //将特性应用到方法 static void PrintOut(string str) { Console.WriteLine(str); }
编译器将无法继续执行。
Conditional特性
Conditional特性允许我们包括或排斥某个特定方法的所有调用。为方法声明应用Condition特性并把编译符作为参数来使用。
- 如果定义了编译符号,那么编译器会包含所有调用这个方法的代码,这和普通方法没有什么区别。
- 如果没有定义编译符号,那么编译器会忽略代码中这个方法的所有调用。
定义方法的CIL代码本身会包含在程序集中。只要调用代码会被插入或忽略。例如,在如下的代码中,把Conditional特性应用到对一个叫做TraceMessage的方法声明上。特性只有一个参数,在这里是字符串DoTrace。
- 当编译器编译这段代码时,它会检查是否有一个编译符号被定义为DoTrace。
- 如果DoTrace被定义,编译器就会像往常一样包信避所有对TraceMessage方法的调用。
- 如果没有DoTrace这样的编译符号被定义,编译器就不会输出任何对TraceMessage的调用代码。
using System.Diagnostics; [Conditional("DoTrace")] static void TraceMessage(string str) { Console.WriteLine(str); }
Conditional特性的示例
以下代码演示了一个使用Conditional特性的完整示例。
- Main方法包含了两个对TraceMessage方法的调用。
- TraceMessage方法的声明被用Conditional特性装饰,它带有DoTrace编译符号作为参数。因此,如果DoTrace被定义,那么编译器就会包含所有对TraceMessage的调用代码。
- 由于代码的第一行定义了叫做DoTrace的编译符,编译器会包含两个对TraceMessage的调用。
#define DoTrace using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Diagnostics; namespace abc { class classCondition { [Conditional("DoTrace")] static void TraceMessage(string str) { Console.WriteLine(str); } static void Main(string[] args) { TraceMessage("abcde"); } } }
预定义的特性
.NET框架预定了很多编译器和CLR能理解和解释的特性,下表列出了一些。在表中使用了不带Attribute后缀的短名称。例如,CLSCompliant的全名是CLSCompliantAttribute。
定义在.NET中的重要特性 特性 意义 CLSCompliant 表明公共暴露的成员应该被编译器检查是否符合CLS。兼容的程序集可以被任何.NET兼容语言使用。 Serializable 表明结构可以被序列化 NonSerialized 声明结构不能被序列化 Obsolete 声明结构不应该再使用。如果使用结构,编译器还会产生编译时警告或者错误信息 DLLImport 表明是非托管代码实现的。 WebMethod
AttributeUsage声明方法应该被作为XML Web服务的一部分暴露
声明特性能应用到什么类型的程序结构。将这个特性应用到特性声明上。
24.7 有 关应用特性的更多内容
多个特性
我们可以为单个结构应用多个我。
- 多个特性可以使用下面列出的任何一种格式:
- 独立的特性片段相互叠在一起;
- 单个特性片段,特性之间使用逗号分隔。
- 我们可以以任何次序列出特性。
其他类型的目标
除了类,我们还可以将特性应用到诸如字段和属民生等其他程序结构。以下的声明显示了字段上的特性以及方法上的多个特性:
我们还可以显式地标注特性,从而将它应用到特殊的目标结构。要使用显式目标,在特性片段的开始处放置目标类型,后面跟冒号。例如,如下的代码用特性装饰方法,并且还把特性应用到返回值上。
[method:MyAttribute("Prints out a message.","Version 3.6")]
[return:MyAttribute("This value represents ...","Version 2.3")]
public long ReturnSetting(){}
C#语言定义了10个标准的特性目标。大多数目标名可以自说明(self-explanatory),而type覆盖了类、结构、委托、枚举和结构。typevar目标名称指定使用泛型结构的类型参数。
event、field、method、param、property、return、type、typevar、assembly、module。
全局特性
我们还可以通过使用assembly和module来使用显式目标把特性设置在程序集或模块级中辊。一些有关程序集级别的特性的要点如下:
- 程序级级别的特性必须放置在任何命名空间之外,并且通常放置在AssemblyInfo.cs文件中。
- AssemblyInfo.cs文件通常包含有关公司、产品以及版权信息的元数据。
24.8 自定义特性
应用特性的语法和之前见过的其他语法很不相同。你可能会觉得特性是和结构完全不同的类型, 其实不是,特性只是某个特殊类型的类。
有关特性类的一些要点如下:
- 用户自定义的特性类叫做自定义特性。
- 所有特性类都派生自System.Attribute。
声明自定义特性
声明一个特性类和声明其他类一样。然而,有一些事项值得注意,如下所示.
- 要声明一个自定义特性,需要做如下工作:
- 声明一个派生自System.Attribute的类。
- 给它起一个以后缀Attribute结尾的名字。
- 为了安全,通常建议你声明一个sealed的特性类。
例如,下面的代码显示了MyAttributeAttribute特性的声明的开始部分:public sealed class MyAttributeAttribute :System.Attribute
{
}
由于特性持有目标的信息,所有特性类的公共成员只能是:
- 字段
- 属性
- 构造函数
使用特性构造函数
特性和其他类一样,都有构造函数。每一个特性至少必须有一个公共构造函数。
- 和其他类一样,如果你不声明构造函数,编译器会为我们产生一个隐式的、公共的、无参的构造函数。
- 特性的构造函数和其他构造函数一样,可以被重载。
- 声明构造函数时必须使用类全名,包括后缀。我们只可以在应用特性时使用短名称。
例如,如果有如下的构造函数(名字没有包含后缀),编译器会产生一个错误信息:public MyAttributeAttribute(string desc,string ver) { Description =desc; VersionNumber = ver; }
指定构造函数
当我们为目标应用特性时,其实是在指定应该使用哪个构造函数来创建特性的实例。列在特性应用中的参数其实就是构造函数的参数。
例如,在下面的代码中,MyAttribute被应用到一个字段和一个方法上。对于字段,声明指定了使用单个字符串的构造函数。对于方法,声明指定了使用两个字符串的构造函数。
[MyAttribute("Helods a value ")] //使用一个字符串的构造函数 public int MyField; [MyAttribute("Version 1.3","Sal Martin")] //使用两个字符串的构造函数 public void MyMethod() { }
其他有关特性构造函数的要点如下:
- 在应用特性时,构造函数的实参必须是在编译期能确定值的常量表达式。
- 如果应用的特性构造函数没有参数,可以省略圆括号。例如,如下代码的两个类都使用MyAttr特性的无参构造函数。两种形式的意义是相同的。
[MyAttr]
class SomeClass .....[MyAttr()]class OtherClass ...
使用构造函数
和其他类一样,我们不能显式调用构造函数。特性的实例创建后就会调用构造函数,只有特性的消费者才能访问特性。这一点与其他类的实例很不相同,这些实例都创建在使用对象创建表达式的位置。应用一个特性是一条声明语句,它不会决定什么时候构造特性类的对象。
比较普通类构造函数的使用和特性的构造函数的使用。
- 命令语句的实际意义是:“在这里创建新的类”。
- 声明语句的意义是:”这个特和这个目标相关联,如果需要构造特性,使用这个构造函数。“
构造函数中的位置参数和命名参数
至此,我们见到的特性构造函数中的参数和普通类构造函数的参数差不多。与普通类型的构造函数一样,特性构造函数的实际必须有正确的次序,而且要与类定义中的形参相匹配。编译器能根据参数列表中参数的位置知道哪个实参和哪个形参相匹配,因此这种参数叫做位置参数。
但是,特性的构造函数还有另外一种类型的实参,叫做命名参数。
- 命名参数设置特性的字段或属性的值。
- 命名参数由字段或属性名后接等号和初始化值构成。
命名参数是实参,它的声明和构造函数的形参是一样的。唯一的区别是特性被应用时提供的实参列表。
如下代码显示了使用一个位置参数和两个命名参数来应用一个特性:
[MyAttribute("An excellent class",Reviewer="Amy McAtthur",ver="0.7.15.33")]
说明:构造函数需要的任何位置参数都必须放在命名参数之前。
限制特性的使用
我们已经看到了可以为类应用特性。而特性本身就是类,有一个很重要的预定义特性可以用来应用到自定义特性上,那就是AttributeUsage特性。我们可以使用它来限制特性用在某个目标类型上。
例如,如果我们希望自定义特性MyAttribute只能应用到方法上,那么可以以如下形式使用AttributeUsage:
[AttributeUsage(AttributeTarget.Method)]
public sealed class MyAttributeAttribute :System.Attribute {...}
AttributeUsage有三个重要的公共属性,如下表所示。表中显示了属性名和属性的含义。对于后两个属性,还显示了它们的默认值。
AttributeUsage的公共属性 名字 意义 默认值 ValidOn 保存特性能应用到的目标类型的列表。构造函数的第一个参数必须是AttributeTarget类型的枚举值。 Inherited 一个布尔值,它指示特性是否会被装饰类型的派生类继承 true AllowMutiple 一个指示目标是否被应用多个特性的实例的布尔值 false
AttributeUsage的构造函数
AttributeUsage的构造函数接受单个位置函数,该参数指定了特性允许的目标类型。它用这个参数来设置ValidOn属性,可接受目标类型是AttributeTarget枚举的成员。AttributeTarget枚举的完整成员列表如下表所示。
AttributeTarget枚举的成员 All Assembly Class Constructor Delegate Enum Event Field GenericParameter Interface Method Module Parameter Property ReturnValue Struct
我们可以通过使用按位或运算符来组合使用类型。例如,在下面的代码中,被装饰的特性只能应用到方法和构造函数上。
[AttributeUsage(AttrubiteTarget.Method | AttributeTaret.Constructor)]
public sealed class MyAttributeAttribute :System.Attribute{...}
当我们为特性声明应用AttributeUsage时,构造函数至少需要一个参数,参数包含的目标类型会保存在ValidOn中。我们还可以通过使用命名参数有选择性地设置Inherited和AllowMultiple属性。如果我们不设置,它们会保持如AttributeUsage的公共属性表中所示的默认值。
作为示例,下面一段代码指定了MyAttribute的如下方面:
- MyAttribute能且只能应用到类上。
- MyAttribute不会被应用它的派生类所继承。
- 不能有MyAttribute的多个实例应用到同一目标上。
[AttributeUsage(AttributeTarget.Class,Inherited = false, AllowMultiple=false)]public sealed class MyAttributeAttribute :System.Attribute {...}
自定义特性的最佳实践
在写自定义特性时,如下的实践是被强烈推荐的:
- 特性类应用表示目标结构的一些状态。
- 如果特性必需某些字段,可以通过包含具有位置参数的构造函数来收集数据,可选字段可以采用命名参数按需初始化。
- 除了属性之外,不要实现公共方法或其他函数成员。
- 为了更安全,把特性类声明为sealed。
- 在特性声明中使用AttributeUsage来显式指定特性目标组。
24.9 访问特性
在本章开始处,我们已经看到了可以使用Type对象来获取类型信息。对于访问自定义特性来说,我们也可以这么做。Type的两个方法在这里非常有用:IsDefined和GetCustomAttribute。
使用IsDefined方法
使用GetCustomAttribute方法我们可以使用type对象的IsDefined方法来检测某个特性是否应用到了某个类上。
例如,以下的代码声明了一个有特性的类MyClass,并且作为自己特性的消费者在程序中访问声明和被应用的特性。在代码的开始处是MyAttribute特性和应用特性的MyClass类的声明。这段代码做了下面的事情:
- 首先,Main创建了类的一个对象。然后通过使用从Object基类继承的GetType方法获取了Type对象的一个引用。
- 有了Type对象的引用,就可以调用IsDefind来判断MyAttribute特性是否应用到了这个类。
- 第一个参数接受需要检查的特性的Type对象。
- 第二个参数是bool类型的,它指示是否搜索MyClass的继承树来查找这个特性。
[AttributeUsage(AttributeTargets.Class)] public sealed class MyAttributeArrtibute : System.Attribute { } [MyAttributeArrtibute] class MyClass { } class Program { static void Main(string[] args) { MyClass mc = new MyClass(); Type t = mc.GetType(); bool isDefined = t.IsDefined(typeof(MyAttributeArrtibute), false); if (isDefined) Console.WriteLine("MyAttribute is applied to type {0}",t.Name); } }
GetCustomAttribute方法返回应用到结构的特性的数组。
- 实际返回的对象是object数组,因此我们必须将它强制转换为相应的特性类型。
- 布尔参数指定是否搜索继承树来查找特性。
object[] AttArr = t.GetcustomAttributes(false);
- 当GetCustomAttributes方法调用后,每一个与目标相关联的特性的实例就会被创建。
下面的代码 使用了前面的示例中相同的特性和类声明。但是,在这种情况下,它不检测特性是否应用到了类的特性的数组,然后遍历它们,输出它们的成员的值。
static void Main(string[] args) { Type t = typeof(MyClass); object[] AttArr = t.GetCustomAttributes(false); foreach (Attribute a in AttArr) { MyAttributeArrtibute attr = a as MyAttributeArrtibute; if (null != attr) { Console.WriteLine("Description :{0}",attr.Description); Console.WriteLine("Version Numer :{0}", attr.VersionNumber); Console.WriteLine("Reviewer ID :{0}", attr.ReviewerID); } } }