技术控

    今日:123| 主题:49331
收藏本版 (1)
最新软件应用技术尽在掌握

[其他] c# 特性

[复制链接]
花心戒了 发表于 2016-10-7 10:03:08
233 1

立即注册CoLaBug.com会员,免费获得投稿人的专业资料,享用更多功能,玩转个人品牌!

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
想想看如果有一个消息系统,它存在这样一个方法,用来将一则短消息发送给某人:
  1. // title: 标题;author:作者;content:内容;receiverId:接受者Id
  2. public bool SendMsg(string title, string author, string content, int receiverId){
  3.     // Do Send Action
  4. }
复制代码
我们很快就发现这样将参数一个个罗列到方法的参数列表中扩展性很糟糕,我们最好定义一个Message类将短消息封装起来,然后给方法传递一个Message对象:
  1. public class Message{
  2.     private string title;
  3.     private string author;
  4.     private string content;
  5.     private int receiverId;
  6.     // 略
  7. }
  8. public bool SendMsg(Messag msg){
  9.     // Do some Action
  10. }
复制代码
此时,我们或许应该将旧的方法删除,用这个扩展性更好的SendMsg方法来取代。遗憾的是我们往往不能,因为这组程序可能作为一组API发布,在很多客户程序中已经在使用旧版本的SendMsg()方法,如果我们在更新程序的时候简单地删除掉旧的SendMsg()方法,那么将造成使用老版本SendMsg()方法的客户程序不能工作。
  这个时候,我们该如果做呢?我们当然可以通过方法重载来完成,这样就不用删除旧的SendMsg()方法了。但是如果新的SendMsg()不仅优化了参数的传递,并且在算法和效率上也进行了全面的优化,那么我们将会迫切希望告知客户程序现在有一个全新的高性能SendMsg()方法可供使用,但此时客户程序并不知道已经存在一个新的SendMsg方法,我们又该如何做呢?我们可以打电话告诉维护客户程序的程序员,或者发电子邮件给他,但这样显然不够方便,最好有一种办法能让他一编译项目,只要存在对旧版本SendMsg()方法的调用,就会被编译器告知。
  .Net 中可以使用特性来完成这一工作。特性是一个对象,它可以加载到程序集及程序集的对象中,这些对象包括 程序集本身、模块、类、接口、结构、构造函数、方法、方法参数等,加载了特性的对象称作特性的目标
  特性的英文名称叫做Attribute,在有的书中,将它翻译为“属性”;另一些书中,将它翻译为“特性”;由于通常我们将含有get和/或set访问器的类成员称为“属性”(英文Property),所以本文中我将使用“特性”这个名词,以区分“属性”(Property)。
  我们通过这个例子来看一下特性是如何解决上面的问题:我们可以给旧的SendMsg()方法上面加上Obsolete特性来告诉编译器这个方法已经过时,然后当编译器发现当程序中有地方在使用这个用Obsolete标记过的方法时,就会给出一个警告信息。
  1. namespace Attribute {
  2.     public class Message {}
  3.    
  4.     public class TestClass {
  5.        // 添加Obsolete特性
  6.        [Obsolete("请使用新的SendMsg(Message msg)重载方法")]
  7.        public static void ShowMsg() {
  8.            Console.WriteLine("这是旧的SendMsg()方法");
  9.        }
  10.        public static void ShowMsg(Message msg) {
  11.            Console.WriteLine("新SendMsg()方法");
  12.        }
  13.     }
  14.     class Program {
  15.        static void Main(string[] args) {
  16.            TestClass.ShowMsg();
  17.            TestClass.ShowMsg(new Message());         
  18.        }
  19.     }
  20. }
复制代码
现在运行这段代码,我们会发现编译器给出了一个警告:警告CS0618: “Attribute.TestClass.ShowMsg()”已过时:“请使用新的SendMsg(Message msg)重载方法”。通过使用特性,我们可以看到编译器给出了警告信息,告诉客户程序存在一个新的方法可供使用,这样,程序员在看到这个警告信息后,便会考虑使用新的SendMsg()方法。
  通过上面的例子,我们已经大致看到特性的使用方法:首先是有一对方括号“[]”,在左方括号“[”后紧跟特性的名称,比如Obsolete,随后是一个圆括号“()”。和普通的类不同,这个圆括号不光可以写入构造函数的参数,还可以给类的属性赋值,在Obsolete的例子中,仅传递了构造函数参数。
  使用构造函数参数,参数的顺序必须同构造函数声明时的顺序相同,所有在特性中也叫位置参数(Positional Parameters),与此相应,属性参数也叫做命名参数(Named Parameters)。
  如果不能自己定义一个特性并使用它,我想你怎么也不能很好的理解特性,我们现在就自己构建一个特性。假设我们有这样一个很常见的需求:我们在创建或者更新一个类文件时,需要说明这个类是什么时候、由谁创建的,在以后的更新中还要说明在什么时候由谁更新的,可以记录也可以不记录更新的内容,以往你会怎么做呢?是不是像这样在类的上面给类添加注释:
  1. //更新:jayce, 2016-9-10, 修改 ToString()方法
  2. //更新:pop, 2016-9-18
  3. //创建:code, 2016-10-1
  4. public class DemoClass{
  5.     // Class Body
  6. }
复制代码
这样的的确确是可以记录下来,但是如果有一天我们想将这些记录保存到数据库中作以备份呢?你是不是要一个一个地去查看源文件,找出这些注释,再一条条插入数据库中呢?
  通过上面特性的定义,我们知道特性可以用于给类型添加元数据(描述数据的数据,包括数据是否被修改、何时创建、创建人,这些数据可以是一个类、方法、属性),这些元数据可以用于描述类型。那么在此处,特性应该会派上用场。那么在本例中,元数据应该是:注释类型(“更新”或者“创建”),修改人,日期,备注信息(可有可无)。而特性的目标类型是DemoClass类。
  按照对于附加到DemoClass类上的元数据的理解,我们先创建一个封装了元数据的类RecordAttribute:
  1. public class RecordAttribute {   
  2.         private string recordType;      // 记录类型:更新/创建   
  3.         private string author;          // 作者   
  4.         private DateTime date;          // 更新/创建 日期   
  5.         private string memo;         // 备注   
  6.       
  7.         // 构造函数,构造函数的参数在特性中也称为“位置参数”。   
  8.         public RecordAttribute(string recordType, string author, string date) {   
  9.            this.recordType = recordType;   
  10.            this.author = author;   
  11.            this.date = Convert.ToDateTime(date);   
  12.         }   
  13.       
  14.         // 对于位置参数,通常只提供get访问器   
  15.         public string RecordType {   get { return recordType; }   }   
  16.         public string Author { get { return author; } }   
  17.         public DateTime Date { get { return date; } }   
  18.       
  19.         // 构建一个属性,在特性中也叫“命名参数”   
  20.         public string Memo {   
  21.            get { return memo; }   
  22.            set { memo = value; }   
  23.         }   
  24.     }
复制代码
注意构造函数的参数 date,必须为一个常量、Type类型、或者是常量数组,所以不能直接传递DateTime类型。
  这个类不光看上去,实际上也和普通的类没有任何区别,显然不能它因为名字后面跟了个Attribute就摇身一变成了特性。那么怎样才能让它称为特性并应用到一个类上面呢?进行下一步之前,我们看看.Net内置的特性Obsolete是如何定义的:
  1. namespace System {   
  2.         [Serializable]   
  3.         [AttributeUsage(6140, Inherited = false)]   
  4.         [ComVisible(true)]   
  5.         public sealed class ObsoleteAttribute : Attribute {   
  6.       
  7.            public ObsoleteAttribute();   
  8.            public ObsoleteAttribute(string message);   
  9.            public ObsoleteAttribute(string message, bool error);   
  10.       
  11.            public bool IsError { get; }   
  12.            public string Message { get; }   
  13.         }   
  14.     }
复制代码
首先,我们应该发现,它继承自Attribute类,这说明我们的 RecordAttribute 也应该继承自Attribute类。 (一个特性类与普通类的区别是:继承了Attribute类)
    其次,我们发现在这个特性的定义上,又用了三个特性去描述它。这三个特性分别是:Serializable、AttributeUsage 和 ComVisible。Serializable特性我们前面已经讲述过,ComVisible简单来说是“控制程序集中个别托管类型、成员或所有类型对 COM 的可访问性”(微软给的定义)这里我们应该注意到:特性本身就是用来描述数据的元数据,而这三个特性又用来描述特性,所以它们可以认为是“元数据的元数据”(元元数据:meta-metadata)。
    (从这里我们可以看出,特性类本身也可以用除自身以外的其它特性来描述,所以这个特性类的特性是元元数据。)
    因为我们需要使用“元元数据”去描述我们定义的特性 RecordAttribute,所以现在我们需要首先了解一下“元元数据”。这里应该记得“元元数据”也是一个特性,大多数情况下,我们只需要掌握 AttributeUsage就可以了,所以现在就研究一下它。我们首先看上面AttributeUsage是如何加载到ObsoleteAttribute特性上面的。
  [AttributeUsage(6140, Inherited = false)]
  然后我们看一下AttributeUsage的定义:
  1. namespace System {
  2.     public sealed class AttributeUsageAttribute : Attribute {
  3.        public AttributeUsageAttribute(AttributeTargets validOn);
  4.        public bool AllowMultiple { get; set; }
  5.        public bool Inherited { get; set; }
  6.        public AttributeTargets ValidOn { get; }
  7.     }
  8. }
复制代码
可以看到,它有一个构造函数,这个构造函数含有一个AttributeTargets类型的位置参数(Positional Parameter) validOn,还有两个命名参数(Named Parameter)。注意ValidOn属性不是一个命名参数,因为它不包含set访问器,(是位置参数)。
  这里大家一定疑惑为什么会这样划分参数,这和特性的使用是相关的。假如AttributeUsageAttribute 是一个普通的类,我们一定是这样使用的:
  1. // 实例化一个 AttributeUsageAttribute 类
  2. AttributeUsageAttribute usage=new AttributeUsageAttribute(AttributeTargets.Class);
  3. usage.AllowMultiple = true;  // 设置AllowMutiple属性
  4. usage.Inherited = false;// 设置Inherited属性
复制代码
但是,特性只写成一行代码,然后紧靠其所应用的类型(目标类型),那么怎么办呢?微软的软件工程师们就想到了这样的办法:不管是构造函数的参数 还是 属性,统统写到构造函数的圆括号中,对于构造函数的参数,必须按照构造函数参数的顺序和类型;对于属性,采用“属性=值”这样的格式,它们之间用逗号分隔。于是上面的代码就减缩成了这样:
  1. [AttributeUsage(AttributeTargets.Class, AllowMutiple=true, Inherited=false)]
复制代码
   可以看出,AttributeTargets.Class是构造函数参数(位置参数),而AllowMutiple 和 Inherited实际上是属性(命名参数)。命名参数是可选的。将来我们的RecordAttribute的使用方式于此相同。(为什么管他们叫参数,我猜想是因为它们的使用方式看上去更像是方法的参数吧。)
    假设现在我们的RecordAttribute已经OK了,则它的使用应该是这样的:
   
  1. [RecordAttribute("创建","张子阳","2008-1-15",Memo="这个类仅供演示")]   
  2.     public class DemoClass{   
  3.         // ClassBody   
  4.     }   
  5.       
  6.     //其中recordType, author 和 date 是位置参数,Memo是命名参数。
复制代码
从AttributeUsage特性的名称上就可以看出它用于描述特性的使用方式。具体来说,首先应该是其所标记的特性可以应用于哪些类型或者对象。从上面的代码,我们看到AttributeUsage特性的构造函数接受一个 AttributeTargets 类型的参数,那么我们现在就来了解一下AttributeTargets。
  AttributeTargets 是一个位标记,它定义了特性可以应用的类型和对象。
  1. public class Message{
  2.     private string title;
  3.     private string author;
  4.     private string content;
  5.     private int receiverId;
  6.     // 略
  7. }
  8. public bool SendMsg(Messag msg){
  9.     // Do some Action
  10. }0
复制代码
因为AttributeUsage是一个位标记,所以可以使用按位或“|”来进行组合。所以,当我们这样写时:
  [AttributeUsage(AttributeTargets.Class|AttributeTargets.Interface)
  意味着既可以将特性应用到类上,也可以应用到接口上。
  AllowMutiple 属性用于设置该特性是不是可以重复地添加到一个类型上(默认为false),就好像这样:
  1. public class Message{
  2.     private string title;
  3.     private string author;
  4.     private string content;
  5.     private int receiverId;
  6.     // 略
  7. }
  8. public bool SendMsg(Messag msg){
  9.     // Do some Action
  10. }1
复制代码
Inherited 就更复杂一些了,假如有一个类继承自我们的DemoClass,那么当我们将RecordAttribute添加到DemoClass上时,DemoClass的子类也会获得该特性。而当特性应用于一个方法,如果继承自该类的子类将这个方法覆盖,那么Inherited则用于说明是否子类方法是否继承这个特性。
  现在实现RecordAttribute应该是非常容易了,对于类的主体不需要做任何的修改,我们只需要让它继承自Attribute基类,同时使用AttributeUsage特性标记一下它就可以了(假定我们希望可以对类和方法应用此特性):
  1. public class Message{
  2.     private string title;
  3.     private string author;
  4.     private string content;
  5.     private int receiverId;
  6.     // 略
  7. }
  8. public bool SendMsg(Messag msg){
  9.     // Do some Action
  10. }2
复制代码
我们已经创建好了自己的自定义特性,现在是时候使用它了。
  1. public class Message{
  2.     private string title;
  3.     private string author;
  4.     private string content;
  5.     private int receiverId;
  6.     // 略
  7. }
  8. public bool SendMsg(Messag msg){
  9.     // Do some Action
  10. }3
复制代码
利用反射来查看 自定义特性信息 与 查看其他信息 类似,首先基于类型(本例中是DemoClass)获取一个Type对象,然后调用Type对象的GetCustomAttributes()方法,获取应用于该类型上的特性。当指定GetCustomAttributes(Type attributeType, bool inherit) 中的第一个参数attributeType时,将只返回指定类型的特性,否则将返回全部特性;第二个参数指定是否搜索该成员的继承链以查找这些属性。
  1. public class Message{
  2.     private string title;
  3.     private string author;
  4.     private string content;
  5.     private int receiverId;
  6.     // 略
  7. }
  8. public bool SendMsg(Messag msg){
  9.     // Do some Action
  10. }4
复制代码
友荐云推荐




上一篇:使用Lombok来精简你的项目代码
下一篇:深入理解 iOS 开发中的锁
酷辣虫提示酷辣虫禁止发表任何与中华人民共和国法律有抵触的内容!所有内容由用户发布,并不代表酷辣虫的观点,酷辣虫无法对用户发布内容真实性提供任何的保证,请自行验证并承担风险与后果。如您有版权、违规等问题,请通过"联系我们"或"违规举报"告知我们处理。

懵懂的青春╮ 发表于 2016-10-29 13:40:29
楼主不许动,我是来抢沙发的,沙发没有,板凳也行!
回复 支持 反对

使用道具 举报

*滑动验证:
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

我要投稿

推荐阅读

扫码访问 @iTTTTT瑞翔 的微博
回页顶回复上一篇下一篇回列表手机版
手机版/CoLaBug.com ( 粤ICP备05003221号 | 文网文[2010]257号 )|网站地图 酷辣虫

© 2001-2016 Comsenz Inc. Design: Dean. DiscuzFans.

返回顶部 返回列表