C#编程(七十一)---------- 自定义特性

自定义特性

在说自定义之前,有必要先介绍一些基本的概念.

元数据:就是C#中封装的一些类,无法修改,类成员的特性被称为元数据中的注释

 

1.什么是特性?

(1)属性和特性的区别

属性:属性是面向对象思想里所说的封装在类里面的数据字段,Get,Set方法.

 

特性:就相当于类的元数据.

 

来看看官方解释?

特性是给指定的某一声明的一则附加的声明性信息。 允许类似关键字的描述声明。它对程序中的元素进行标注,如类型、字段、方法、属性等。从.net角度看,特性是一种 类,这些类继承于System.Attribute类,用于对类、属性、方法、事件等进行描述,主要用在反射中。但从面向对象的级别看,其实Attribute是类型级别的,而不是对象级别。

 

 

Attribute和.net文件的元数据保存在一起,可以用来向运行时描述你的代码,或者在程序运行的时候影响程序的行为.

 

2.特性的应用

(1).net中特性用来处理多种问题,比如序列化,程序的安全特性,防止即时编译器对程序代码进行优化从而代码容易调试等等.定值特性的本质上是在一个类的元素上添加附加信息,并在运行其通过反射得带该附加信息(在使用数据实体对象时经常用到)

(2)Attribute作为编译期的指令时的应用

Conditional起条件编译的作用,只有满足条件,才允许编译器对它的代码进行编译.一般在程序调试的时候使用.

DllImport:用来标记非.net函数,表明该方法在一个外部的DLL定义.

Obsolete:这个属性用来标记当前的方法已被废弃,不再使用.

 

注意:Attribute是一个类,因此DllImport也是一个类,Attribute类是在编译的时候实例化,而不是想通常那样在运行时实例化.

CLSCompliant:保证整个程序集代码遵守CLS,否则编译将报错.

 

3.自定义特性

使用AttributeUsage,来控制如何应用新定义的特性.

 

  [AttributeUsageAttribute(AttributeTargets.All  可以应用到任何元素

      ,AllowMultiple=true, 允许应用多次,我们的定值特性能否被重复放在同一个程序实体前多次。

      ,Inherited=false,不继承到派生

        )]

特性也是一个类,必须继承于System.Attribute类,命名规范我类名+Attribute.不管直接还是间接继承,都会成为一个特性类,特性类的声明定义了一种可以放置在声明之上新的特性.使用[]语法使用自定义特性,可以使用反射来查看自定义特性.

案例:

public class MyselfAttribute:System.Attribute

 

不过说实话,特性确实常用到,但是自定义特性几乎用不到,貌似老外喜欢用.

 

如果不能自己定义一个特性并使用它,我姓你肯定觉得我在忽悠你,假设我们有这样一个很常见的需求:我们在创建或者更新一个类文件时,需要说要这个类时什么时候,由谁创建的,在一行的更新中还要说明在什么时候由谁更新的,可以记录也可以不记录更新的内容,以往的情况你会怎么做?肯定想到了添加注释:

//更新:张三,2015-2-3,修改了ToString()方法

//更新:李四,2014-4-5

//创建:王五,2011-7-8

public class DemoClass

{

//dosomething

}

这样做没问题,想要啥,就写啥,看起来很好啊,借用金星老师的一句话就是完美!

but,如果我们有一天想把这些记录保存到数据库中作为备份呢?你是不是要一条一条的去查看源文件,找出注释,然后在一条条的插入到数据库中呢?

 

通过上面特性的定义,我们知道特性可以用来给类型添加元数据(描述书觉得数据,包括数据是否被修改,何时创建,创建人,这些数据可以是一个类,方法,属性),这些元数据可以用于描述类型.那么在此处,特性就会派上用场.那么在本例中,元数据应该是:注释类型(更行或者创建),修改人,日期,备注信息(可有可无).而特性的目标类型是DemoClass类.

 

按照对于附加到DemoClass类上的元数据的理解,我们先创建一个封装了元数据的类:

    public class RecordAttribute

    {

        private string recordType;//记录类型:更新或者创建

        private string author;//作者

        private DateTime data;//日期

        private string memo;//备注

 

 

        //构造函数的参数在特性中也称为"位置参数"

        public RecordAttribute(string  recordType,string author,string  date)

        {

            this.recordType = recordType;

            this.author = author;

            this.data = 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特性比较简单,就是一个标志,AttributeUsage比较重要,有三个重要的参数可以设置,上面说了.

 

这里我们应该可以注意到:特性本身就是用来描述数据的元数据,而这三个特性又用来描述特性,所以他们是”元数据的元数据”(元元数据).

 

从这里我们可以看出,特性类本身也可以用除自身以外的其他特性来描述,所以这个特性类的特性数元元数据.

 

隐隐我们需要使用元元数据全无描述我们定义的特性RecordAttribute,所以现在我们需要首先了解一下”元元数据”.这里应该记得”元元数据”也是一个特性,大多数情况下,我们只需要掌握AttributeUsage就够了.现在我们深入的研究一下它.先来看一下上面AttributeUsage是如何加载到ObsoleteAttribute特性上面的.

[AttributeUsage(6140,Inheritedfalse)]

看看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;//设置AllowMultiple属性

usage.Inherited=false;//设置Inherited属性

 

但是,特性只写成一行代码,然后紧靠其所应用的类型(目标类型),那么咋办呢?大牛们想到了:不管是构造函数的参数还是属性,彤彤写到构造函数的圆括号中,对于构造函数的参数,必须按照构造函数参数的顺序和类型;对于属性,采用”属性=值”这样的格式,他们之间用逗号分隔.于是上面的代码就缩减成了下面这样:

[AttributeUsage(AttributeTargets.Class,AllowMutliple=true,Inherited=false)]

可以看出,AttributeTargets.Class是构造函数的参数(位置参数),而AllowMutliple和Inherited实际上是属性(命名参数).命名参数是可选的.将来我们的RecordAttribute的使用方式于此相同.(为什么管这些属性叫做作数,可能是因为它们的使用方式看上去更像方法的参数)

假设现在我们的RecordAttribute已经OK了,则它的使用使用应该是这样的:

[RecordAttribute(“创建”,”syx”,”2015-8-8”,Memo=”hello,world”)]

public class DemoClass

{

//dosomething

}

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

 

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

从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是一个标记,所以可以使用按位或”|”来进行组合.so,当我们这样写的时候:

[AttributeUsage(AttributeTargets.Class|AttributeTargets.Interface)

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

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

 

 

C#自定义特性:实现RecordAttribute

现在实现RecordAttribute应该很轻松了,对于类的主题不需要进行任何的修改,我们只是需要让这个类继承自Attribute类,同时使用AttributeUsage特性标记一下它就可以了(假定我们希望可以对类和方法应用此特性):

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

public class recordAttribute:Attribute

{

//主体

}

 

完整代码:

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Threading.Tasks;

 

namespace 自定义特性

{

    class Program

    {

        static void Main(string[] args)

        {

            DemoClass demo = new DemoClass();

            Console.WriteLine(demo.ToString());

            Console.ReadKey();

        }

    }

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

    public class RecordAttribute : Attribute

    {

        private string recordType;//记录类型:更新或者创建

        private string author;//作者

        private DateTime data;//日期

        private string memo;//备注

 

 

        //构造函数的参数在特性中也称为"位置参数"

        public RecordAttribute(string recordType, string author, string date)

        {

            this.recordType = recordType;

            this.author = author;

            this.data = 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; }

        }

    }

    [Record("更新", "wangwu", "2008-1-20", Memo = "修改 ToString()方法")]

    [Record("更新", "lisi", "2008-1-18")]

    [Record("创建", "zhangsan", "2008-1-15")]

    public class DemoClass

    {

        public override string ToString()

        {

            return "hello,world!";

        }

    }

 

}

 

 

这段程序可能简单的输出”hello,world”.我们的属性也好像使用”//”来注释一样对程序没有任何影响,实际上,我们添加的数据已经作为元数据添加到程序集中.

至此,一个完整的自定义特性的使用已经完成了,举个例子帮助你理解特性打个比方:你约一个没见过面的网友约会,约好时间地点,怎么解决不认识TA的问题?你们可以约好,手上拿个特别的东西不就解决了。这个特别的、用于标识你所不认识的人的东西,就相当于Attribute了。所以Attribute是用于在运行期动态调用的场合。

如果仅仅是前面介绍的内容,还是不足以说明Attribute有什么实用价值的话,那么从后面的章节开始我们将介绍几个Attribute的不同用法,相信你一定会对Attribute有一个新的了解。

posted @ 2017-09-28 14:07  贫道俺来也  阅读(199)  评论(0编辑  收藏  举报