想想看如果有一個訊息系統,它存在這樣一個方法,用來將一則短訊息發送給某人:
// title: 标题;author:作者;content:内容;receiverId:接受者Id public bool SendMsg(string title, string author, string content, int receiverId){ // Do Send Action }
我們很快就發現這樣將參數一個個羅列到方法的參數列表中擴展性很糟糕,我們最好定義一個Message類別將短訊息封裝起來,然後給方法傳遞一個Message物件:
public class Message{ private string title; private string author; private string content; private int receiverId; // 略 } public bool SendMsg(Messag msg){ // Do some Action }
此時,我們或許應該將舊的方法刪除,用這個擴展性更好的SendMsg方法來取代。遺憾的是我們往往不能,因為這組程式可能會作為一組API發布,在許多客戶程式中已經在使用舊版的SendMsg()方法,如果我們在更新程式的時候簡單地刪除掉舊的SendMsg()方法,那麼將造成使用舊版SendMsg()方法的客戶程式不能運作。
這個時候,我們該如果做呢?我們當然可以透過方法重載來完成,這樣就不用刪除舊的SendMsg()方法了。但是如果新的SendMsg()不僅優化了參數的傳遞,並且在演算法和效率上也進行了全面的優化,那麼我們將會迫切希望告知客戶程式現在有一個全新的高效能SendMsg()方法可供使用,但此時客戶程式並不知道已經存在一個新的SendMsg方法,我們又該如何做呢?我們可以打電話告訴維護客戶程式的程式設計師,或者發電子郵件給他,但這樣顯然不夠方便,最好有辦法能讓他一編譯項目,只要存在舊版SendMsg()方法的調用,就會被編譯器告知。
.Net 中可以使用特性來完成這項工作。特性是一個對象,它可以載入到組件及程序集的對像中,這些對象包括程序集本身、模組、類別、介面、結構、建構子、方法、方法參數等,載入了特性的對象稱作特性的目標
特性的英文名稱叫做Attribute,在有的書中,將它翻譯為“屬性”;另一些書中,將它翻譯為“特性”;由於通常我們將含有get和/或set訪問器的類別成員稱為「屬性」(英文Property),所以本文中我將使用「特性」這個名詞,以區分「屬性」(Property)。
我們透過這個例子來看一下特性是如何解決上面的問題:我們可以給舊的SendMsg()方法上面加上Obsolete特性來告訴編譯器這個方法已經過時,然後當編譯器發現當程式中有地方在使用這個用Obsolete標記過的方法時,就會給予一個警告訊息。
namespace Attribute { public class Message {} public class TestClass { // 添加Obsolete特性 [Obsolete("请使用新的SendMsg(Message msg)重载方法")] public static void ShowMsg() { Console.WriteLine("这是旧的SendMsg()方法"); } public static void ShowMsg(Message msg) { Console.WriteLine("新SendMsg()方法"); } } class Program { static void Main(string[] args) { TestClass.ShowMsg(); TestClass.ShowMsg(new Message()); } } }
現在運行這段程式碼,我們會發現編譯器給了一個警告:警告CS0618: “Attribute.TestClass.ShowMsg()”已過時:“請使用新的SendMsg(Message msg)重載方法”。透過使用特性,我們可以看到編譯器給出了警告訊息,告訴客戶程式存在一個新的方法可供使用,這樣,程式設計師在看到這個警告訊息後,便會考慮使用新的SendMsg()方法。
透過上面的例子,我們已經大致看到特性的使用方法:首先是有一對方括號“[]”,在左方括號“[”後緊跟特性的名稱,比如Obsolete,隨後是一個圓括號“ ()」。和普通的類別不同,這個圓括號不光可以寫入建構子的參數,還可以給類別的屬性賦值,在Obsolete的例子中,只傳遞了建構子參數。
使用建構子參數,參數的順序必須與建構子宣告時的順序相同,所有在特性中也叫位置參數(Positional Parameters),與此相應,屬性參數也叫做命名參數(Named Parameters)。
如果不能自己定義一個特性並使用它,我想你怎麼也不能很好的理解特性,我們現在就自己構建一個特性。假設我們有這樣一個很常見的需求:我們在創建或更新一個類別文件時,需要說明這個類別是什麼時候、由誰創建的,在以後的更新中還要說明在什麼時候由誰更新的,可以記錄也可以不記錄更新的內容,以往你會怎麼做呢?是不是像這樣在類的上面給類別添加註釋:
//更新:jayce, 2016-9-10, 修改 ToString()方法 //更新:pop, 2016-9-18 //创建:code, 2016-10-1 public class DemoClass{ // Class Body }
這樣的確確是可以記錄下來,但是如果有一天我們想將這些記錄保存到資料庫中作以備份呢?你是不是要一個一個地去查看源文件,找出這些註釋,再一條條插入數據庫中呢?
通過上面特性的定義,我們知道特性可以用於給類型新增元資料(描述資料的數據,包括資料是否被修改、何時建立、建立人,這些資料可以是一個類別、方法、屬性),這些資料可以用來描述類型。那麼在此處,特性應該會派上用場。那麼在本例中,元資料應該是:註解類型(「更新」或「建立」),修改人,日期,備註資訊(可有可無)。而特性的目標類型是DemoClass類別。
按照對於附加到DemoClass類別上的元資料的理解,我們先創建一個封裝了元資料的類別RecordAttribute:
public class RecordAttribute { private string recordType; // 记录类型:更新/创建 private string author; // 作者 private DateTime date; // 更新/创建 日期 private string memo; // 备注 // 构造函数,构造函数的参数在特性中也称为“位置参数”。 public RecordAttribute(string recordType, string author, string date) { this.recordType = recordType; this.author = author; this.date = Convert.ToDateTime(date); } // 对于位置参数,通常只提供get访问器 public string RecordType { get { return recordType; } } public string Author { get { return author; } } public DateTime Date { get { return date; } } // 构建一个属性,在特性中也叫“命名参数” public string Memo { get { return memo; } set { memo = value; } } }
注意建構函數的參數date,必須為一個常數、Type類型、或者是常數數組,或者是常數數組,或者是常數數組,或者是常數數組,所以不能直接傳遞DateTime類型。
这个类不光看上去,实际上也和普通的类没有任何区别,显然不能它因为名字后面跟了个Attribute就摇身一变成了特性。那么怎样才能让它称为特性并应用到一个类上面呢?进行下一步之前,我们看看.Net内置的特性Obsolete是如何定义的:
namespace System { [Serializable] [AttributeUsage(6140, Inherited = false)] [ComVisible(true)] public sealed class ObsoleteAttribute : Attribute { public ObsoleteAttribute(); public ObsoleteAttribute(string message); public ObsoleteAttribute(string message, bool error); public bool IsError { get; } public string Message { get; } } }
首先,我们应该发现,它继承自Attribute类,这说明我们的 RecordAttribute 也应该继承自Attribute类。 (一个特性类与普通类的区别是:继承了Attribute类)
其次,我们发现在这个特性的定义上,又用了三个特性去描述它。这三个特性分别是:Serializable、AttributeUsage 和 ComVisible。Serializable特性我们前面已经讲述过,ComVisible简单来说是“控制程序集中个别托管类型、成员或所有类型对 COM 的可访问性”(微软给的定义)这里我们应该注意到:特性本身就是用来描述数据的元数据,而这三个特性又用来描述特性,所以它们可以认为是“元数据的元数据”(元元数据:meta-metadata)。(从这里我们可以看出,特性类本身也可以用除自身以外的其它特性来描述,所以这个特性类的特性是元元数据。)
因为我们需要使用“元元数据”去描述我们定义的特性 RecordAttribute,所以现在我们需要首先了解一下“元元数据”。这里应该记得“元元数据”也是一个特性,大多数情况下,我们只需要掌握 AttributeUsage就可以了,所以现在就研究一下它。我们首先看上面AttributeUsage是如何加载到ObsoleteAttribute特性上面的。
[AttributeUsage(6140, Inherited = false)]
然后我们看一下AttributeUsage的定义:
namespace System { public sealed class AttributeUsageAttribute : Attribute { public AttributeUsageAttribute(AttributeTargets validOn); public bool AllowMultiple { get; set; } public bool Inherited { get; set; } public AttributeTargets ValidOn { get; } } }
可以看到,它有一个构造函数,这个构造函数含有一个AttributeTargets类型的位置参数(Positional Parameter) validOn,还有两个命名参数(Named Parameter)。注意ValidOn属性不是一个命名参数,因为它不包含set访问器,(是位置参数)。
这里大家一定疑惑为什么会这样划分参数,这和特性的使用是相关的。假如AttributeUsageAttribute 是一个普通的类,我们一定是这样使用的:
// 实例化一个 AttributeUsageAttribute 类 AttributeUsageAttribute usage=new AttributeUsageAttribute(AttributeTargets.Class); usage.AllowMultiple = true; // 设置AllowMutiple属性 usage.Inherited = false;// 设置Inherited属性
但是,特性只写成一行代码,然后紧靠其所应用的类型(目标类型),那么怎么办呢?微软的软件工程师们就想到了这样的办法:不管是构造函数的参数 还是 属性,统统写到构造函数的圆括号中,对于构造函数的参数,必须按照构造函数参数的顺序和类型;对于属性,采用“属性=值”这样的格式,它们之间用逗号分隔。于是上面的代码就减缩成了这样:
[AttributeUsage(AttributeTargets.Class, AllowMutiple=true, Inherited=false)]
可以看出,AttributeTargets.Class是构造函数参数(位置参数),而AllowMutiple 和 Inherited实际上是属性(命名参数)。命名参数是可选的。将来我们的RecordAttribute的使用方式于此相同。(为什么管他们叫参数,我猜想是因为它们的使用方式看上去更像是方法的参数吧。)假设现在我们的RecordAttribute已经OK了,则它的使用应该是这样的:
[RecordAttribute("创建","张子阳","2008-1-15",Memo="这个类仅供演示")] public class DemoClass{ // ClassBody } //其中recordType, author 和 date 是位置参数,Memo是命名参数。
从AttributeUsage特性的名称上就可以看出它用于描述特性的使用方式。具体来说,首先应该是其所标记的特性可以应用于哪些类型或者对象。从上面的代码,我们看到AttributeUsage特性的构造函数接受一个 AttributeTargets 类型的参数,那么我们现在就来了解一下AttributeTargets。
AttributeTargets 是一个位标记,它定义了特性可以应用的类型和对象。
public enum AttributeTargets { Assembly = 1, //可以对程序集应用属性。 Module = 2, //可以对模块应用属性。 Class = 4, //可以对类应用属性。 Struct = 8, //可以对结构应用属性,即值类型。 Enum = 16, //可以对枚举应用属性。 Constructor = 32, //可以对构造函数应用属性。 Method = 64, //可以对方法应用属性。 Property = 128, //可以对属性 (Property) 应用属性 (Attribute)。 Field = 256, //可以对字段应用属性。 Event = 512, //可以对事件应用属性。 Interface = 1024, //可以对接口应用属性。 Parameter = 2048, //可以对参数应用属性。 Delegate = 4096, //可以对委托应用属性。 ReturnValue = 8192, //可以对返回值应用属性。 GenericParameter = 16384, //可以对泛型参数应用属性。 All = 32767, //可以对任何应用程序元素应用属性。 }
因为AttributeUsage是一个位标记,所以可以使用按位或“|”来进行组合。所以,当我们这样写时:
[AttributeUsage(AttributeTargets.Class|AttributeTargets.Interface)
意味着既可以将特性应用到类上,也可以应用到接口上。
AllowMutiple 属性用于设置该特性是不是可以重复地添加到一个类型上(默认为false),就好像这样:
[RecordAttribute("更新","jayce","2016-1-20")] [RecordAttribute("创建","pop","2016-1-15",Memo="这个类仅供演示")] public class DemoClass{ // ClassBody }
Inherited 就更复杂一些了,假如有一个类继承自我们的DemoClass,那么当我们将RecordAttribute添加到DemoClass上时,DemoClass的子类也会获得该特性。而当特性应用于一个方法,如果继承自该类的子类将这个方法覆盖,那么Inherited则用于说明是否子类方法是否继承这个特性。
现在实现RecordAttribute应该是非常容易了,对于类的主体不需要做任何的修改,我们只需要让它继承自Attribute基类,同时使用AttributeUsage特性标记一下它就可以了(假定我们希望可以对类和方法应用此特性):
[AttributeUsage(AttributeTargets.Class|AttributeTargets.Method, AllowMultiple=true, Inherited=false)] public class RecordAttribute:Attribute { // 略 }
我们已经创建好了自己的自定义特性,现在是时候使用它了。
[Record("更新", "code", "2016-1-20", Memo = "修改 ToString()方法")] [Record("更新", "jayce", "2016-1-18")] [Record("创建", "pop", "2016-1-15")] public class DemoClass { public override string ToString() { return "This is a demo class"; } } class Program { static void Main(string[] args) { DemoClass demo = new DemoClass(); Console.WriteLine(demo.ToString()); } }
利用反射来查看 自定义特性信息 与 查看其他信息 类似,首先基于类型(本例中是DemoClass)获取一个Type对象,然后调用Type对象的GetCustomAttributes()方法,获取应用于该类型上的特性。当指定GetCustomAttributes(Type attributeType, bool inherit) 中的第一个参数attributeType时,将只返回指定类型的特性,否则将返回全部特性;第二个参数指定是否搜索该成员的继承链以查找这些属性。
class Program { static void Main(string[] args) { Type t = typeof(DemoClass); Console.WriteLine("下面列出应用于 {0} 的RecordAttribute属性:" , t); // 获取所有的RecordAttributes特性 object[] records = t.GetCustomAttributes(typeof(RecordAttribute), false); foreach (RecordAttribute record in records) { Console.WriteLine(" {0}", record); Console.WriteLine(" 类型:{0}", record.RecordType); Console.WriteLine(" 作者:{0}", record.Author); Console.WriteLine(" 日期:{0}", record.Date.ToShortDateString()); if(!String.IsNullOrEmpty(record.Memo)){ Console.WriteLine(" 备注:{0}",record.Memo); } } } }