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有一个新的了解。