想想如果有一个消息系统,它有这样一个方法可以给某人发一条短信://title:title;作者:作者;内容:内容;receiverId:receiverIdpublicboolSendMsg(stringtitle,stringauthor,stringcontent,intreceiverId){//DoSendAction}我们很快发现,将参数一一列出到方法的参数列表中的可扩展性很差。我们最后定义了一个Message类来封装短信,然后传递一个Message对象给方法:publicclassMessage{privatestringtitle;privatestringauthor;privatestringcontent;privateintreceiverId;//abbreviated}publicboolSendMsg(Messagmsg){//DosomeAction}此时,我们或许应该删除旧的方法,用更具扩展性的SendMsg方法来代替。不幸的是,我们往往做不到,因为这组程序可能会作为一组API发布,而旧版本的SendMsg()方法已经在许多客户端程序中使用。如果我们在更新程序方法时简单地删除旧的SendMsg()方法,那么使用旧版本SendMsg()方法的客户端程序将无法工作。这个时候,我们该怎么办?当然,我们可以通过方法重载来实现,这样就不需要删除旧的SendMsg()方法了。但是如果新的SendMsg()不仅优化了参数的传递,还充分优化了算法和效率,那么我们就会迫不及待地通知客户端程序现在有一个新的高性能SendMsg()方法可用了,但是这时候客户端程序并不知道有一个新的SendMsg方法,怎么办呢?我们可以打电话给维护客户端程序的程序员,或者发邮件给他,但是这样显然不方便,**有办法让他编译工程,只要有老版本的调用即可SendMsg()方法,编译器会通知他。.Net中提供了属性来执行此操作。特性是一个可以加载到程序集的对象及其对象,包括程序集本身、模块、类、接口、结构体、构造函数、方法、方法参数等。加载特性的对象称为特性的英文名称目标特征称为属性。在一些书中,它被翻译为“属性”;在其他书中,它被翻译为“特色”;成员被称为“属性”(英文Property),所以在本文中我将使用“特征”一词来区分“属性”(Property)。我们通过这个例子来看看该特性是如何解决上面的问题的:我们可以在旧的SendMsg()方法中加入Obsolete特性来告诉编译器这个方法已经过时了,然后当编译器发现有一个地方在programwhereitused当该方法被标记为Obsolete时,会给出警告信息。namespaceAttribute{publicclassMessage{}publicclassTestClass{//AddObsolete属性[Obsolete("请使用新的SendMsg(Messagemsg)重载方法")]publicstaticvoidShowMsg(){Console.WriteLine("这是旧的SendMsg()方法");}publicstaticvoidShowMsg(Messagemsg){Console.WriteLine("新的SendMsg()方法");}}classProgram{staticvoidMain(string[]args){TestClass.ShowMsg();TestClass.ShowMsg(newMessage());}}}现在运行这段代码,我们会发现编译器给出警告:WarningCS0618:"Attribute.TestClass.ShowMsg()"isobsolete:"PleaseusethenewSendMsg(Messagemsg)overloadmethod"。通过使用该特性,我们可以看到编译器给出一个警告信息,告诉客户端程序有一个新的方法可用,这样程序员在看到这个警告信息后就会考虑使用新的SendMsg()方法。通过上面的例子,我们大致看到了feature的使用方法:首先是一个方括号“[]”,左方括号“[”后面是feature的名称,比如Obsolete,后面是一个括号“()”。与普通类不同的是,括号不仅可以写入构造函数的参数,还可以赋值给类的属性。在Obsolete的例子中,只传递了构造函数的参数。使用构造函数参数时,参数的顺序必须与构造函数声明的顺序相同。所有的属性也称为位置参数(PositionalParameters),相应地,属性参数也称为命名参数(NamedParameters)。如果你不能自己定义一个特性并使用它,我认为你不能很好地理解这个特性。现在让我们自己构建一个功能。假设我们有这样一个很常见的需求:当我们创建或更新一个类文件时,我们需要说明这个类是什么时候、由谁创建的,在以后的更新中又是什么时候、谁更新的。你可以记录也可以不记录更新的内容。过去你会做什么?你是不是这样给类添加注释://Update:jayce,2016-9-10,修改ToString()方法//Update:pop,2016-9-18//create:code,2016-10-1publicclassDemoClass{//ClassBody}确实可以记录下来,但是万一有一天我们想把这些记录保存到数据库中备份呢?是不是要一一查看源文件,把这些评论找出来,一一插入数据库?通过上面特征的定义,我们知道特征可以用来给类型添加元数据(描述数据的数据,包括数据是否被修改,创建时间,创建者,这些数据可以是一个类,method,attribute),这些元数据可以用来描述类型。那么,在这里,属性应该派上用场。所以在这个例子中,元数据应该是:评论类型(“更新”或“创建”),修改人,日期,备注信息(可选)。该功能的目标类型是DemoClass类。根据DemoClass类附带的元数据的理解,我们首先创建一个封装元数据的类RecordAttribute:publicclassRecordAttribute{privatestringrecordType;//recordtype:update/createprivatestringauthor;//authorprivateDateTimedate;//update/createdateprivatestringmemo;//备注//构造函数,构造函数的参数在特性中也称为“位置参数”。publicRecordAttribute(stringrecordType,stringauthor,stringdate){this.recordType=recordType;this.author=author;this.date=Convert.ToDateTime(date);}//对于位置参数,通常只提供get访问器publicstringRecordType{get{returnrecordType;}}publicstringAuthor{get{returnauthor;}}publicDateTimeDate{get{returndate;}}//构建一个属性,在特征中也称为“命名参数”publicstringMemo{get{returnmemo;}set{memo=value;}}}注意构造函数的参数date必须是常量、Type类型或常量数组,不能直接传DateTime类型。这个班不仅看起来,实际上和普通班没什么区别。显然,不能仅仅因为名称后跟一个属性就把它变成一个特征。那么如何才能称之为特征并应用到类中呢?在进行下一步之前,我们先看看.Net的内置特性Obsolete是如何定义的:;publicObsoleteAttribute(stringmessage);publicObsoleteAttribute(stringmessage,boolerror);publicboolIsError{get;}publicstringMessage{get;}}}首先,我们应该发现,这说明我们的RecordAttribute也应该继承自Attribute类。(属性类和普通类的区别在于它继承了Attribute类。)其次,我们发现在这个属性的定义中,多了三个属性来描述它。这三个属性是:Serializable、AttributeUsage和ComVisible。Serializable特性我们之前已经提到过,ComVisible只是简单的“控制各个托管类型,成员,或者程序集中的所有类型对COM的可访问性”(微软给出的定义)这里要注意:该特性本身是用元数据描述的数据,而这三个特征是用来描述特征的,所以可以认为是“元数据的元数据”(meta-metadata:元数据)。(从这里我们可以看出,要素类本身也可以通过自身以外的其他要素来描述,所以这个要素类的要素就是meta-metadata。)因为我们需要用“meta-metadata”来描述我们定义的要素RecordAttribute,所以现在我们需要先了解“元元数据”。在这里你应该记住“元元数据”也是一个特性。大多数情况下,我们只需要掌握AttributeUsage,那么现在就来研究一下吧。我们先看看上面的AttributeUsage是如何加载到ObsoleteAttribute特性中的。[AttributeUsage(6140,Inherited=false)]然后我们看一下AttributeUsage的定义:,它有一个构造函数,其中包含一个类型为AttributeTargets的位置参数(PositionalParameter)validOn,和两个命名参数(NamedParameter)。请注意,ValidOn属性不是命名参数,因为它不包含集合访问器(它是位置参数)。看到这里大家一定很疑惑为什么参数会这样划分,这跟feature的使用有关。如果AttributeUsageAttribute是一个普通的类,我们必须这样使用它:usage.AllowMultiple=true;//设置AllowMultiple属性usage.Inherited=false;//设置Inherited属性但是属性只写了一行代码,然后在类型(目标类型)旁边它被应用了,那我该怎么办?微软的软件工程师想到了这样一个办法:无论是构造函数的参数还是属性,都写在构造函数的括号中。对于构造函数的参数,必须遵循构造函数参数的顺序和类型;对于属性,使用“attribute=value”的格式,用逗号分隔。所以上面的代码简化为:[AttributeUsage(AttributeTargets.Class,AllowMutiple=true,Inherited=false)]可以看出AttributeTargets.Class是一个构造函数参数(位置参数),而AllowMutiple和I??nherited其实是属性(命名参数)。命名参数是可选的。以后我们的RecordAttribute也会这样使用。(为什么叫参数,我猜是因为用起来更像方法参数。)假设现在我们的RecordAttribute就OK了,它的使用应该是这样的:[RecordAttribute("create","张子扬","2008-1-15",Memo="Thisclassisonlyfordemonstration")]publicclassDemoClass{//ClassBody}//其中recordType、author和date是位置参数,Memo是命名参数。从AttributeUsage属性的名称可以看出,它是用来描述属性的用法的。具体来说,首先应该是标记的特征可以应用于哪些类型或对象。从上面的代码中我们看到AttributeUsage特性的构造函数接受了一个AttributeTargets类型的参数,那么我们现在来看一下AttributeTargets。AttributeTargets是一个位标志,它定义了属性可以应用的类型和对象。publicenumAttributeTargets{Assembly=1,//属性可以应用到程序集。Module=2,//属性可以应用于模块。Class=4,//属性可以应用于类。struct=8,//属性可以应用于结构,即值类型。Enum=16,//可以对枚举应用属性。Constructor=32,//属性可以应用于构造函数。Method=64,//属性可以应用于方法。Property=128,//可以对属性应用属性。Field=256,//可以对字段应用属性。Event=512,//可以对事件应用属性。Interface=1024,//可以对接口应用属性。Parameter=2048,//可以对参数应用属性。Delegate=4096,//属性可以应用于委托。ReturnValue=8192,//可以对返回值应用属性。GenericParameter=16384,//属性可以应用于泛型参数。All=32767,//属性可以应用于任何应用元素。}因为AttributeUsage是位标志,可以使用bitwise或者"|"结合。所以,当我们这样写的时候:[AttributeUsage(AttributeTargets.Class|AttributeTargets.Interface)意味着属性可以应用于类和接口。AllowMutiple属性用于设置特征是否可以重复添加到一个类型中(默认为false),像这样:[RecordAttribute("Update","jayce","2016-1-20")][RecordAttribute("Create","pop","2016-1-15",Memo="这个类只是为了演示")]publicclassDemoClass{//ClassBody}继承比较复杂,如果有一个类继承自我们的DemoClass,那么当我们为DemoClass添加RecordAttribute时,DemoClass的子类也会得到这个特性。而当该特性应用于一个方法时,如果从该类继承的子类重写了该方法,则使用Inherited表示该子类方法是否继承该特性。现在实现RecordAttribute应该很容易了。无需对类的主体进行任何修改。我们只需要让它继承自Attribute基类,并用AttributeUsage属性来标记即可(假设我们希望能够在class和method上应用这个特性):[AttributeUsage(AttributeTargets.Class|AttributeTargets.Method,AllowMultiple=true,Inherited=false)]publicclassRecordAttribute:Attribute{//short}我们已经创建了自己的自定义属性,现在是时候使用它了。[Record("Update","code","2016-1-20",Memo="修改ToString()方法")][Record("Update","jayce","2016-1-18")][Record("create","pop","2016-1-15")]publicclassDemoClass{publicoverridestringToString(){return"Thisisademoclass";}}classProgram{staticvoidMain(string[]args){DemoClassdemo=newDemoClass();控制台.WriteLine(demo.ToString());}}使用反射查看自定义属性信息与查看其他信息类似。首先根据类型(本例为DemoClass)获取Type对象,然后调用Type对象的GetCustomAttributes()方法。,以获取应用于该类型的属性。当指定GetCustomAttributes(TypeattributeType,boolinherit)中的第一个参数attributeType时,只返回指定类型的属性,否则返回所有属性;第二个参数指定是否搜索成员的继承链来查找这些属性。classProgram{staticvoidMain(string[]args){Typet=typeof(DemoClass);Console.WriteLine("以下是应用于{0}的RecordAttribute属性:",t);//获取所有RecordAttributes属性object[]records=t.GetCustomAttributes(typeof(RecordAttribute),false);foreach(RecordAttributerecordinrecords){Console.WriteLine("{0}",record);Console.WriteLine("Type:{0}",record.RecordType);控制台。WriteLine("作者:{0}",record.Author);Console.WriteLine("日期:{0}",record.Date.ToShortDateString());if(!String.IsNullOrEmpty(record.Memo)){Console.WriteLine("Memo:{0}",record.Memo);}}}}
