Note程序员
- 类的元数据包含该类的成员和特性
- 程序的元数据能够理解为程序的结构信息
- 反射(reflection)用来查看元数据
- C#中经过Type类来反射
- 特性(attribute)用来给类型添加元数据
PS:理解有待增强数组
大多数程序都要处理数据,包括读、写、操做和显示数据。(图形也是一种数据的形式。)然而,对于某些程序来讲,它们操做的数据不是数字、文本或图形,而是程序和程序类型自己的信息。浏览器
对象浏览器是显式元数据的程序的一个示例。它能够读取程序集,而后显示所包含的类型以及类型的全部特性和成员。
本章将介绍程序如何使用Type类来反射数据,以及程序员如何使用特性来给类型添加元数据。安全
要使用反射,咱们必须使用System.Reflection命名空间。app
以前已经介绍了如何声明和使用C#中的类型。包括预约义类型(int、long和string等)、BCL中的类型(Console、IEnumerable等)以及用户自定义类型(MyClass、Mydel等)。每一种类型都有本身的成员和特性。
BCL声明了一个叫作Type的抽象类,它被设计用来包含类型的特性。使用这个类的对象能让咱们获取程序使用的类型的信息。
因为Type是抽象类,所以它不能有实例。而是在运行时,CLR建立从Type(RuntimeType)派生的类的实例,Type包含了类型信息。当咱们要访问这些实例时,CLR不会返回派生类的引用而是Type基类的引用。可是,为了简单起见,在本章剩余的篇幅中,我会把引用所指向的对象称为Type类型的对象(虽然从技术角度来讲是一个BCL内部的派生类型的对象)。
须要了解的有关Type的重要事项以下:框架
下图显示了一个运行的程序,它有两个MyClass对象和一个OtherClass对象。注意,尽管有两个MyClass的实例,只会有一个Type对象来表示它。
咱们能够从Type对象中获取须要了解的有关类型的几乎全部信息。下表列出了类中更有用的成员。
函数
本节学习使用GetType方法和typeof运算符来获取Type对象。object类型包含了一个叫作GetType的方法,它返回对实例的Type对象的引用。因为每个类型最终都是从object继承的,因此咱们能够在任何类型对象上使用GetType方法来获取它的Type对象,以下所示: 学习
Type t = myInstance.GetType();
下面的代码演示了如何声明一个基类以及从它派生的子类。Main方法建立了每个类的实例而且把这些引用放在了一个叫作bca的数组中以方便使用。在外层的foreach循环中,代码获得了Type对象而且输出类的名字,而后获取类的字段并输出。下图演示了内存中的对象。
spa
using System; using System.Reflection; 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 t = typeof(DerivedClass); ↑ ↑ 运算符 但愿的Type对象的类型
下面的代码给出了一个使用typeof运算符的简单示例:
using System; using System.Reflection; namespace SimpleReflection { class BaseClass { public int MyFieldBase; } class DerivedClass:BaseClass { public int MyFieldDerived; } 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); } } } }
特性(attribute)是一种容许咱们向程序的程序集增长元数据的语言结构。它是用于保存程序结构信息的某种特殊类型的类。
下图是使用特性中相关组件的概览,而且也演示了以下有关特性的要点。
根据惯例,特性名使用Pascal命名法而且以Attribute后缀结尾。当为目标应用特性时,咱们能够不使用后缀。例如,对于SerializableAttribute和MyAttributeAttribute这两个特性,咱们在把它们应用到结构时可使用Serializable和MyAttribute短名称。
咱们先不讲解如何建立特性,而是看看如何使用已定义的特性。这样,你会对它们的使用状况有个大体了解。
特性的目的是告诉编译器把程序结构的某组元数据嵌入程序集。咱们能够经过把特性应用到结构来实现。
例如,下面的代码演示了两个类的开始部分。最初的几行代码演示了把一个叫作Serializable的特性应用到MyClass。注意,Serializable没有参数列表。第二个类的声明有一个叫作MyAttribute的特性,它有一个带有两个string参数的参数列表。
[Serializable] public class MyClass { … } [MyAttribute("Simple class","Version 3.57")] public class MyOtherClass { … }
有关特性须要了解的重要事项以下:
在学习如何定义本身的特性以前,本小节会先介绍几个.NET预约义特性。
一个程序可能在其生命周期中经历屡次发布,并且极可能延续多年。在程序生命周期的后半部分,程序员常常须要编写相似功能的新方法替换老方法。出于多种缘由,你可能不想再使用那些调用过期的旧方法的老代码,而只想用新编写的代码调用新方法。
若是出现这种状况,你确定但愿稍后操做代码的团队成员或程序员也只使用新代码。要警告他们不要使用旧方法,可使用Obsolete特性将程序结构标注为过时的,而且在代码编译时显式有用的警告消息。如下代码给出了一个使用的示例:
class Program { //应用特性 [Obsolete("User 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("User method SuperPrintOut",true)] static void PrintOut(string str) { … }
Note
Conditional特性相似于C语言的条件编译
Conditional特性容许咱们包括或排斥特定方法的全部调用。为方法声明应用Conditional特性并把编译符做为参数来使用。
定义方法的CIL代码自己老是会包含在程序集中。只是调用代码会被插入或忽略。
例如,在以下的代码中,把Conditional特性应用到对一个叫作TraceMessage的方法的声明上。特性只有一个参数,在这里是字符串DoTrace。
[Conditional("DoTrace")] static void TraceMessage(string str) { Console.WriteLine(str); }
Conditional特性的示例
如下代码演示了一个使用Conditional特性的完整示例。
#define DoTrace using System; using System.Diagnostics; namespace AttributeConditional { class Program { [Conditional("DoTrace")] static void TraceMessage(string str) { Console.WriteLine(str); } static void Main() { TraceMessage("Start of Main"); Console.WriteLine("Doing work in Main."); TraceMessage("End of Main"); } } }
若是注释掉第一行来取消DoTrace的定义,编译器就再也不会插人两次对TraceMessage的调用代码。此次,若是咱们运行程序,就会产生以下输出:
调用者信息特性能够访问文件路径、代码行数、调用成员的名称等源代码信息。
CallerFilePath
、CallerLineNumber
和CallerMemberName
下面的代码声明了一个名为MyTrace的方法,它在三个可选参数上使用了这三个调用者信息特性。若是调用方法时显式指定了这些参数,则会使用真正的参数值。但在下面所示的Main方法中调用时,没有显式提供这些值,所以系统将会提供源代码的文件路径、调用该方法的代码行数和调用该方法的成员名称。
using System; using System.Runtime.CompilerServices; public static class Program { public static void MyTrace(string message, [CallerFilePath] string fileName="", [CallerLineNumber] int lineNumber=0, [CallerMemberName] string callingMember="") { Console.WriteLine("File: {0}",fileName); Console.WriteLine("Line: {0}",lineNumber); Console.WriteLine("Called From: {0}",callingMember); Console.WriteLine("Message: {0}",message); } public static void Main() { MyTrace("Simple message"); } }
咱们在单步调试代码时,经常但愿调试器不要进入某些方法。咱们只想执行该方法,而后继续调试下一行。DebuggerStepThrough特性告诉调试器在执行目标代码时不要进入该方法调试。
在我本身的代码中,这是最常使用的特性。有些方法很小而且毫无疑问是正确的,在调试时对其反复单步调试只能徒增烦恼。但使用该特性时要十分当心,由于你并不想排除那些可能含有bug的代码。
关于DebuggerStepThrough要注意如下两点:
下面这段随手编造的代码在一个访问器和一个方法上使用了该特性。你会发现,调试器调试这段代码时不会进入IncrementFields方法或X属性的set访问器。
using System; using System.Diagnostics; class Program { int _x=1; int X { get{return _x;} [DebuggerStepThrough] set { _x=_x*2; _x+=value; } } public int Y{get;set;} public static void Main() { var p=new Program(); p.IncrementFields(); p.X=5; Console.WriteLine("X = {0}, Y = {1}",p.X,p.Y); } [DebuggerStepThrough] void IncrementFields() { X++; Y++; } }
.NET框架预约义了不少编译器和CLR能理解和解释的特性,下表列出了一些。在表中使用了不带Attribute后缀的短名称。例如,CLSCompliant的全名是CLSCompliantAttribute。
至此,咱们演示了特性的简单使用,都是为方法应用单个特性。这部份内容将会讲述其余特性的使用方式。
咱们能够为单个结构应用多个特性。
例如,下面的两个代码片断显示了应用多个特性的两种方式。两个片断的代码是等价的。
[Serializable ] //多层结构 [MyAttribute("Simple class", "Version 3.57")] [MyAttribute("Simple class", "Version 3.57"),Serializable] //逗号分隔
除了类,咱们还能够将特性应用到诸如字段和属性等其余程序结构。如下的声明显示了字段上的特性以及方法上的多个特性:
[MyAttribute("Holds a value", "Version 3.2")] //字段上的特性 public int MyField; [Obsolete] //方法上的特性 [MyAttribute("Prints out a message.", "Version 3.6")] public void Printout() { … }
咱们还能够显式地标注特性,从而将它应用到特殊的目标结构。要使用显式目标,在特性片断的开始处放置目标类型,后面跟冒号。例如,以下的代码用特性装饰方法,而且还把特性应用到返回值上。
显式目标说明符 ↓ [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目标名称指定使用泛型结构的类型参数。
咱们还能够经过使用assembly和module目标名称来使用显式目标说明符把特性设置在程序集或模块级別。(程序集和模块在第21章中解释过。)一些有关程序集级别的特性的要点以下:
以下的代码行摘自AssemblyInfo.cs文件:
[assembly: AssemblyTitle("SuperWidget")] [assembly: AssemblyDescription("Implements the SuperWidget product.")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("McArthur Widgets, Inc.")] [assembly: AssemblyProduct("Super Widget Deluxe")] [assembly: AssemblyCopyright("Copyright © McArthur Widgets 2012")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")]
你或许已经注意到了,应用特性的语法和以前见过的其余语法很不相同。你可能会以为特性是和结构彻底不一样的类型,其实不是,特性只是某个特殊类型的类。
有关特性类的一些要点以下。
整体来讲,声明一个特性类和声明其余类同样。然而,有一些事项值得注意,以下所示。
例如,下面的代码显示了MyAttributeAttribute特性的声明的开始部分:
特性名 基类 ↓ ↓ public sealed class MyAttributeAttribute : System.Attribute { … }
因为特性持有目标的信息,全部特性类的公共成员只能是:
特性和其余类同样,都有构造函数。每个特性至少必须有一个公共构造函数。
例如,若是有以下的构造函数(名字没有包含后缀),编译器会产生一个错误消息:
public MyAttributeAttribute(string desc,string ver) { Description=desc; VersionNumber=ver; }
当咱们为目标应用特性时,实际上是在指定应该使用哪一个构造函数来建立特性的实例。列在特性应用中的参数其实就是构造函数的参数。
例如,在下面的代码中,MyAttribute被应用到一个字段和一个方法上。对于字段,声明指定了使用单个字符串的构造函数。对于方法,声明指定了使用两个字符串的构造函数。
[MyAttribute("Holds a value")] //使用一个字符串的构造函数 public int MyField; [MyAttribute("version 1.3", "Sal Martin")] //使用两个字符串的构造函数 public void MyMethod() { … }
其余有关特性构造函数的要点以下。
[MyAttr] class SomeClass … [MyAttr()] class OtherClass …
和其余类同样,咱们不能显式调用构造函数。特性的实例建立后,只有特性的消费者访问特性时才能调用构造函数。这一点与其余类的实例很不相同,这些实例都建立在使用对象建立表达式的位置。应用一个特性是一条声明语句,它不会决定何时构造特性类的对象。
下图比较了普通类构造函数的使用和特性的构造函数的使用。
和普通类的方法与构造方法类似,特性的构造方法一样可使用位置参数和命名参数。以下代码显示了使用一个位置参数和两个命名参数来应用一个特性:
位置参数 命名参数 命名参数 ↓ ↓ ↓ [MyAttribute("An excellent class",Reviewer="Amy McArthur",Ver="0.7.15.33")]
下面的代码演示了特性类的声明以及为MyClass类应用特性。注意,构造函数的声明只列出了一个形参,但咱们可经过命名参数给构造函数3个实参。两个命名参数设置了字段Ver和Reviewer的值。
public sealed class MyAttributeAttribute : System.Attribute { public string Description; public string Ver; public string Reviewer; public MyAttributeAttribute(string desc) //一个形参 { Description = desc; } } //三个实参 [MyAttribute("An excellent class”, Reviewer="Amy McArthur", Ver="7.15.33")] class MyClass { … }
构造函教须要的任何位置参数都必须放在命名参数以前。
咱们已经看到了能够为类应用特性。而特性自己就是类,有一个很重要的预约义特性能够用来应用到自定义特性上,那就是AttributeUsage特性。咱们可使用它来限制特性使用在某个目标类型上。
例如,若是咱们但愿自定义特性MyAttribute只能应用到方法上,那么能够以以下形式使用AttributeUsage:
只针对方法 ↓ [AttributeUsage( AttributeTarget.Method )] public sealed class MyAttributeAttribute : System.Attribute { … }
AttributeUsage有三个重要的公共属性,以下表所示。表中显示了属性名和属性的含义。对于后两个属性,还显示了它们的默认值。
AttributeUsage的构造函数
AttributeUsage的构造函数接受单个位置参数,该参数指定了特性容许的目标类型。它用这个参数来设置ValidOn属件,可接受目标类型是AttributeTarget枚举的成员。AttributeTarget枚举的完整成员列表以下表所示。
咱们能够经过使用按位或运算符来组合使用类型。例如,在下面的代码中,被装饰的特性只能应用到方法和构造函数上。
目标 ↓ [AttributeUsage( AttributeTarget.Method| AttributeTarget.Constructor )] public sealed class MyAttributeAttribute : System.Attribute
当咱们为特性声明应用AttributeUsage时,构造函数至少须要一个参数,参数包含的目标类型会保存在ValidOn中。咱们还能够经过使用命名参数有选择性地设置Inherited和AllowMultiple属性。若是咱们不设置,它们会保持如表24-4所示的默认值。
做为示例,下面一段代码指定了MyAttribute的以下方面。
[AttributeUsage( AttributeTarget.Class, //必需的,位置参数 Inherited = false, //可选的,命名参数 AllowMultiple = false )] //可选的,命名参数 public sealed class MyAttributeAttribute : System.Attribute { … }
强烈推荐编写自定义特性时参考以下实践。
以下代码演示了这些准则:
[AttributeUsage( AttributeTargets.Class )] public sealed class ReviewCommentAttribute : System.Attribute { public string Description {get;set;} public string VersionNumber {get;set;} public string ReviewerID {get;set;} public ReviewCommentAttribute(string desc, string ver) { Description = desc; VersionNumber = ver; } }
在本章开始处,咱们已经看到了可使用Type对象来获取类型信息。对于访问自定义特性来讲,咱们也能够这么作。Type的两个方法(IsDefined和GetCustomAttributes)在这里很是有用。
咱们可使用Type对象的IsDefined方法来检测某个特性是否应用到了某个类上。
例如,如下的代码声明了一个有特性的类MyClass,而且做为本身特性的消费者在程序中访问声明和被应用的特性。代码的开始处是MyAttribute特性和应用特性的MyClass类的声明。这段代码作了下面的事情。
[AttributeUsage(AttributeTargets.Class)] public sealed class ReviewCommentAttribute:System.Attribute {…} [ReviewComment("Check it out","2.4")] class MyClass{} class Program { static void Main() { var mc=new MyClass(); Type t=mc.GetType(); bool isDefined= t.IsDefined(typeof(ReviewCommentAttribute),false); if(isDefined) Console.WriteLine("ReviewComment is applied to type {0}",t.Name); } }
GetCustomAttributes方法返回应用到结构的特性的数组。
object[] AttArr = t.GetCustomAttributes(false);
下面的代码使用了前面的示例中相同的特性和类声明。可是,在这种状况下,它不检测特性是否应用到了类,而是获取应用到类的特性的数组,而后遍历它们,输出它们的成员的值。
using System; [AttributeUsage(AttributeTargets.Class)] public sealed class MyAttributeAttribute:System.Attribute { public string Description {get;set;} public string VersionNumber{get;set;} public string ReviewerID {get;set;} public MyAttributeAttribute(string desc,string ver) { Description=desc; VersionNumber=ver; } } [MyAttribute("Check it out","2.4")] class MyClass { } class Program { static void Main() { Type t=typeof(MyClass); object[] AttArr=t.GetCustomAttributes(false); foreach(Attribute a in AttArr) { var attr=a as MyAttributeAttribute; if(null!=attr) { Console.WriteLine("Description :{0}",attr.Description); Console.WriteLine("Version Number :{0}",attr.VersionNumber); Console.WriteLine("Reviewer ID :{0}",attr.ReviewerID); } } } }