C#自定义特性介绍

开发 后端
本文通过实例介绍了C#自定义特性。如果不能自己定义一个特性并使用它,我想你怎么也不能很好的理解特性。

C#自定义特性范例介绍

如果不能自己定义一个特性并使用它,我想你怎么也不能很好的理解特性,我们现在就自己构建一个特性。假设我们有这样一个很常见的需求:我们在创建或者更新一个类文件时,需要说明这个类是什么时候、由谁创建的,在以后的更新中还要说明在什么时候由谁更新的,可以记录也可以不记录更新的内容,以往你会怎么做呢?是不是像这样在类的上面给类添加注释:

  1. //更新:Matthew, 2008-2-10, 修改 ToString()方法  
  2. //更新:Jimmy, 2008-1-18  
  3. //创建:张子阳, 2008-1-15  
  4. public class DemoClass{  
  5.     // Class Body  

这样的的确确是可以记录下来,但是如果有一天我们想将这些记录保存到数据库中作以备份呢?你是不是要一个一个地去查看源文件,找出这些注释,再一条条插入数据库中呢?

通过上面特性的定义,我们知道特性可以用于给类型添加元数据(描述数据的数据,包括数据是否被修改、何时创建、创建人,这些数据可以是一个类、方法、属性),这些元数据可以用于描述类型。那么在此处,特性应该会派上用场。那么在本例中,元数据应该是:注释类型(“更新”或者“创建”),修改人,日期,备注信息(可有可无)。而特性的目标类型是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. }    
  25.  

NOTE:注意构造函数的参数 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.  
  5.        public bool AllowMultiple { getset; }  
  6.        public bool Inherited { getset; }  
  7.        public AttributeTargets ValidOn { get; }  
  8.     }  
  9. }  
  10.  

可以看到,它有一个构造函数,这个构造函数含有一个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了,则它的使用应该是这样的:

C#代码

  1. [RecordAttribute("创建","张子阳","2008-1-15",Memo="这个类仅供演示")]     
  2. public class DemoClass{     
  3.     // ClassBody     
  4. }     

其中recordType, author 和 date 是位置参数,Memo是命名参数。

C#自定义特性:AttributeTargets 位标记

从AttributeUsage特性的名称上就可以看出它用于描述特性的使用方式。具体来说,首先应该是其所标记的特性可以应用于哪些类型或者对象。从上面的代码,我们看到AttributeUsage特性的构造函数接受一个 AttributeTargets 类型的参数,那么我们现在就来了解一下AttributeTargets。

AttributeTargets 是一个位标记,它定义了特性可以应用的类型和对象。

[Flags]

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(AttributeTargets.Class, AllowMutiple=true, Inherited=false)]

而ObsoleteAttribute特性上加载的 AttributeUsage是这样的:

[AttributeUsage(6140, Inherited = false)]

因为AttributeUsage是一个位标记,所以可以使用按位或“|”来进行组合。所以,当我们这样写时:

[AttributeUsage(AttributeTargets.Class|AttributeTargets.Interface)

意味着既可以将特性应用到类上,也可以应用到接口上。

NOTE:这里存在着两个特例:观察上面AttributeUsage的定义,说明特性还可以加载到程序集Assembly和模块Module上,而这两个属于我们的编译结果,在程序中并不存在这样的类型,我们该如何加载呢?可以使用这样的语法:[assembly:SomeAttribute(parameter list)],另外这条语句必须位于程序语句开始之前。

C#自定义特性:Inherited 和 AllowMutiple属性

AllowMutiple 属性用于设置该特性是不是可以重复地添加到一个类型上(默认为false),就好像这样:

[RecordAttribute("更新","Jimmy","2008-1-20")]

[RecordAttribute("创建","张子阳","2008-1-15",Memo="这个类仅供演示")]

public class DemoClass{

// ClassBody

}

所以,我们必须显示的将AllowMutiple设置为True。

Inherited 就更复杂一些了,假如有一个类继承自我们的DemoClass,那么当我们将RecordAttribute添加到DemoClass上时,DemoClass的子类也会获得该特性。而当特性应用于一个方法,如果继承自该类的子类将这个方法覆盖,那么Inherited则用于说明是否子类方法是否继承这个特性。

在我们的例子中,将 Inherited 设为false。

C#自定义特性:实现 RecordAttribute

现在实现RecordAttribute应该是非常容易了,对于类的主体不需要做任何的修改,我们只需要让它继承自Attribute基类,同时使用AttributeUsage特性标记一下它就可以了(假定我们希望可以对类和方法应用此特性):

[AttributeUsage(AttributeTargets.Class|AttributeTargets.Method, AllowMultiple=true, Inherited=false)]

public class RecordAttribute:Attribute {

    // 略

}

C#自定义特性:使用 RecordAttribute

我们已经创建好了自己的自定义特性,现在是时候使用它了。

C#代码

  1. [Record("更新""Matthew""2008-1-20", Memo = "修改 ToString()方法")]     
  2. [Record("更新""Jimmy""2008-1-18")]     
  3. [Record("创建""张子阳""2008-1-15")]     
  4. public class DemoClass {          
  5.     public override string ToString() {     
  6.        return "This is a demo class";     
  7.     }     
  8. }     
  9.     
  10. class Program {     
  11.     static void Main(string[] args) {     
  12.        DemoClass demo = new DemoClass();     
  13.        Console.WriteLine(demo.ToString());     
  14.     }     
  15. }    

这段程序简单地在屏幕上输出一个“This is a demo class”。我们的属性也好像使用“//”来注释一样对程序没有任何影响,实际上,我们添加的数据已经作为元数据添加到了程序集中。可以通过IL DASM看到:

可以通过IL DASM看到 

【编辑推荐】

  1. C#基础知识一览
  2. 学习C#自定义用户控件
  3. C#自定义组件和用户组件属性的设置
  4. C#编程中的组件-事件-委托
  5. Visual C#自定义组件的设计:Pop3Com组件
责任编辑:book05 来源: cnblogs
相关推荐

2009-08-04 09:09:51

C#反射

2009-08-28 17:45:19

C#自定义数据

2009-08-04 12:56:51

C#自定义事件

2021-03-29 00:02:10

C#Attribute元素

2009-09-03 15:46:57

C#自定义事件

2009-08-03 13:34:06

自定义C#控件

2009-08-03 13:39:46

C#自定义用户控件

2009-08-05 17:03:37

C#自定义控件

2009-08-04 09:56:46

C#事件处理自定义事件

2021-06-17 06:52:37

C#自定义异常

2009-08-04 12:40:34

c#自定义事件

2009-08-12 14:53:50

C#类型转换函数

2009-08-04 08:48:44

C#内置特性

2009-08-04 13:31:35

C#自定义事件

2009-08-05 18:01:20

C#自定义异常处理

2009-08-04 13:07:46

C#自定义快捷键

2009-08-17 17:24:02

C#自定义消息框

2009-08-03 16:37:49

C#异常类

2009-08-05 17:15:27

C#自定义按钮

2009-08-03 14:46:12

C#自定义控件
点赞
收藏

51CTO技术栈公众号