前言
上一篇简单的介绍了一下Attribute的基础概念以及应用场景和方法,这一篇文章,我们就来聊聊如何自定义一个特性
实例
需求:
在创建或者更新一个类文件时,需要说明这个类是什么时候由谁创建的,在以后的更新中还要说明什么时候偶是由谁更新的,可以记录也可以不记录更新的内容,以往我们可以通过注释的方式在类上边添加注释:
//更新:Celine,2017年5月29日,修改了ToString()方法
public class DemoClass {
public override string ToString()
{
return "This is a demo class";
}
}
但是如果有一天想要查看所有类型的更新记录怎么办呢?是不是一个一个的去查看源文件,找出这些注释?
咱们再来回顾一下特性的定义:特性可以用于给类型添加元数据,这些元数据用于描述类型。那么我们是不是可以将这些描述信息通过特性来为类进行添加,在这个例子中,要附加的类型元素应该是:注释类型(“更新”或者“创建”)、修改人、日期、备注信息(可有可无)
1.先创建一个封装了元数据的类RecordAttribute:
public class RecordAttribute:Attribute
{
private string recordType; //记录类型:更新/创建
private string author; //作者
private DateTime date; //更新/创建日期
private string memo; //备注
//构造函数,构造函数的参数在特性中也成为“位置参数”
public RecordAttribute(string recordType,string author,string date){
//特性类的构造函数的参数有一些限制:必须为敞亮、Type类型,或者是常量数组
//因此不能直接传递DateTime类型,只能传递String类型,然后在构造函数内进行一个强制类型转换
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; } }
//构建一个属性,在特性中也叫“命名参数”
public string Memo
{
get;
set;
}
}
这样定义是不够的,因为上看去就是一个普通的类没有任何区别,我们先看一下上一篇中用到的预定义特性: Obsolete是如何写的:
// 摘要:
// 标记不再使用的程序元素。 此类不能被继承。
[Serializable]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Constructor | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Interface | AttributeTargets.Delegate, Inherited = false)]
[ComVisible(true)]
public sealed class ObsoleteAttribute : Attribute
{
public ObsoleteAttribute();
public ObsoleteAttribute(string message);
ol error);
public bool IsError { get; }
public string Message { get; }
}
注意一下几个特点:
- 继承自Attribute类
- 在Obsolete上边有用了三个特性去描述他
- Serializable:支持序列化
- AttributeUsage:帮助我们控制定制特性的使用
- ComVisible :对COM的可访问性
他们是描述元数据的特性,所以可以成为元元数据(meta-metadata)
如果是自定义的特性,我们只需要用到AttributeUsage这一个特性就可以了,那么,我们就来看看AttributeUsage是如何定义的:
// 摘要:
// 指定另一特性类的用法。 此类不能被继承。
[Serializable]
[AttributeUsage(AttributeTargets.Class, Inherited = true)]
[ComVisible(true)]
public sealed class AttributeUsageAttribute : Attribute
{
public AttributeUsageAttribute(AttributeTargets validOn);
public bool AllowMultiple { get; set; }
public bool Inherited { get; set; }
public AttributeTargets ValidOn { get; }
}
特点:
- 有一个构造函数,这个构造函数含有一个AttributeTargets的位置参数
- 有两个命名参数(AllowMultiple、Inherited)
根据特性的书写规范,必须写成一行,位于所应用的目标类型上,就会采用一种特殊的写法:不管是构造函数的参数还是属性,全部写到构造函数的圆括号中,对于构造函数的参数,必须采取构造函数的参数的顺序和类型,因此叫做位置参数;对于属性,采用“属性”=“值”的格式,他们之间用逗号分隔,称作命名参数
我们的RecordAttribute如果写好了,那么在使用的时候就应该是如下所示
[Record("更新", "Celine", "2017年5月29日", Memo="修改ToString(方法)")]
public class DemoClass{
}
其中的recordType,author和date是位置参数,Memo是命名参数
在AttributeUsage中的构造函数接受一个AttributeTargets类型的参数,而这个AttributeTargets是一个位标记,他表示这个特性可以加载在哪些类型上,如果写成AttributeTargets.Class,代表可以应用于类这个类型上
// 指定可以对它们应用特性的应用程序元素。
[Serializable]
[ComVisible(true)]
[Flags]
public enum AttributeTargets
{
Assembly = 1,
Module = 2,
Class = 4,
Struct = 8,
Enum = 16,
Constructor = 32,
Method = 64,
Property = 128,
Field = 256,
Interface = 1024,
Delegate = 4096,
ReturnValue = 8192,
GenericParameter = 16384,
All = 32767,
}
AllowMuitple:设置该特性是不是可以重复地添加到一个类型上,例如
[Record("更新", "Celine", "2017年5月29日", Memo="修改ToString(方法)")]
[Record("更新", "Celine", "2017年5月28日")]
[Record("创建", "Celine", "2017年5月28日")]
public class DemoClass {
public override string ToString()
{
return "This is a demo class";
}
}
Inherited:是否能够被继承
2.实现RecordAttribute
只需要使用AttributeUsage来标注这个类就可以了,主体代码不需要改变
[AttributeUsage(AttributeTargets.Class|AttributeTargets.Method,AllowMultiple=true,Inherited=false)]
[Record("更新", "Celine", "2017年5月29日", Memo="修改ToString(方法)")]
[Record("更新", "Celine", "2017年5月28日")]
[Record("创建", "Celine", "2017年5月28日")]
public class DemoClass {
public override string ToString()
{
return "This is a demo class";
}
}
static void Main(string[] args)
{
DemoClass demo = new DemoClass();
Console.WriteLine(demo.ToString());
Console.ReadLine();
}
}
3.使用反射来查看元数据
Type t = typeof(DemoClass);
Console.WriteLine("下面列出应用于{0}的RecordAttribute属性:", t);
//获取所有的ReconrdAttribute特性
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);
}
}
Console.ReadLine();
总结
这两篇博客介绍了.NET的两种很重要的应用:特性+反射,特性说的够多了,现在一句话说说反射,反射提供这样几个能力:查看和遍历类型和类型成员的元数据;动态创建类型实例,动态调用所创建的势力的方法,字段,属性(机房中的反射,动态的创建一个接口);迟绑定方法和属性,这里用到的第一个功能,以后有机会在研究别的功能。