我们初学C#的时候看到类上面一对中括号里面有个高亮了的关键字,不知道那是什么有什么用。想问人又不知道它叫什么。纠结的要命。其实,它就是特性。如:
这就是我们今天要分析的主题。
个人理解,特性就是修饰对象元数据的修饰符。
元数据就是用来描述数据的数据。 (挺拗口的)
如:
图中的1.是特性 2.是访问修饰符 3.声明修饰符 4.数据类型 5.变量名 6.变量数据值,其中1、2、3、4、5就是元数据,用来描述数据(6)的数据。
如上面的 Obsolete ,会不会也是一个如 public static 这样类似的修饰符呢,我们且看看反编译后的中间语言。
意料之外,我们看到了上面的2、3、4、5,而1 (特性) 怎么跑到里面去了,且是一种看不懂的东东,反正我们知道了不是类似的修饰符。
然后我们接着在vs里面把光标移到 Obsolete 上按F12,如:
原来只是一个继承了 Attrbute 的一个类 (class) 。那么上面我们看不懂的部分应该就是这个 ObsoleteAttribute 类的实例化了。
我们来回答上面问题:特性到底是什么?特性只是一个类而已。
我们看到上面系统特性 Obsolete 上面还有特性,如:Serializable、AttributeUsage、Camvisible等。像这种特性我们称之为“元数据的元数据”(元元数据)。
1.我们分别来解释性上面的三个特性。
Serializable:表示类型支持序列化。
ComVisible:微软定义“控制程序集中个别托管类型、 成员或所有类型对COM的可访问性”。
AttributeUsage:这个比较重要了,基本上每个特性定义都用到了它。它就是用来表示当前这个特性可用于哪些对象。如:类、方法、属性...等等。( 只需要用到这个我们就可以自定义特性了 )
2.上面有个问题,不知道大家发现没有。
就是我们特性名明明是 Obsolete ,为什么我们F12进去后变成了 ObsoleteAttribute 呢?这其实只是一个微软的约定而已,没有为什么。
其实我们可以两种写法: [ObsoleteAttribute( " 已过时 " )] 和 [Obsolete( " 已过时 " )] 是等效的,只是我们一般都用后面这种。
3.定义的特性必须继承于 Attribute 。
4.属性没有set方法。只能通过构造函数赋值。 (这是因为特性语法所致,因为特性的定义只存在单行的中括号中,不能实例化之后在设置属性,所以全部的设置都在后面的小括号里进行的)
好了,我们通过这四点完全可以自己定义个特性来玩玩了。我们来定义一个给机器看的注释。我们平时的注释都只是给程序员看的,编译之后就全没了。那我们想在代码运行时,弹出我们的注释怎么办,接下来我们用自定义特性来实现,如:
[AttributeUsage(AttributeTargets.All)]//3.设置可用于哪些对象 public class TMessgAttribute : Attribute//1.定义类TMessg加上后缀TMessgAttribute 2.继承Attribute。 { public TMessgAttribute() { } /// <param name="createTime">创建时间</param> /// <param name="createName">创建人</param> public TMessgAttribute(string createTime, string createName, string mess) { this._createName = createName; this._createTime = createTime; this._mess = mess; } private string _createTime; public string createTime { get { return _createTime; }//4.只能有get方法 } private string _createName; public string createName { get { return _createName; } } private string _mess; public string mess { get { return _mess; } } }
好了,上面就是我们自定义的特性。那我们怎样使用呢。和系统特性一样。我们先定义一个测试类TClass,然后在类上面定义特性,如:
[TMessg("2015-12-20", "zhaopei", "我只是测试自定义特性,不要报错哦,求求你了。")] public class TClass { //................ }
我们定义了特性,也使用了特性,然我们却不知道怎么看效果。我们想看到效果怎么办。可以使用反射 (下篇博问继续分析反射) 看看 TClass 类的元数据,如:
static void Main(string[] args) { System.Reflection.MemberInfo info = typeof(TClass); //通过反射得到TClass类的信息 TMessgAttribute hobbyAttr = (TMessgAttribute)Attribute.GetCustomAttribute(info, typeof(TMessgAttribute)); Console.WriteLine("类名:{0}", info.Name); Console.WriteLine("创建时间:{0}", hobbyAttr.createTime); Console.WriteLine("创建人:{0}", hobbyAttr.createName); Console.WriteLine("备注消息:{0}", hobbyAttr.mess); Console.ReadKey(); }
打印效果如:
上面的自定义特性都是通过构造函数设置字段私有字段,然后通过只提供了get的属性来访问。那么可否直接在特性里面定义拥有get和set的属性吗?答案是肯定的。那怎么在使用特性的时候设置这个属性呢?我们接着往下看。
我们接着在自定义特性里面添加一个属性。
/// <summary> /// 修改时间 /// </summary> public string modifyTime { get; set; }
使用自定义特性。
[TMessg("2015-12-20", "zhaopei", "我只是测试自定义特性,不要报错哦,求求你了。", modifyTime = "2015-12-21")] public class TClass { //................ }
我们发现,直接在输入了构造函数之后接着设置属性就可以。( 这就相当于可选参数了,属性当然可以随便你是否设置了。不过这里需要注意了,前面的参数一定要按照定义的特性构造函数的参数顺序 )
这种参数,我们成为命名参数。
我们F12看看AttributeUsage的定义
看上去,同样也只是普通的特性。实际上也只是个普通的特性。>_<
我们来看看他的这几个属性是干嘛的。从最后一个开始看。
1.AttributeTargets,我们在上面其实就已经看到并也已经使用了。
我们设置的是可用于所有对象。AttributeTargets其实是个枚举,每个值对于一个类型对象。
你可以直接在 AttributeTargets F12进去:
我们看到了每个值代表可以用于所对于的对象类型。
2.Inherited (是一个布尔值) :“ 如果该属性可由派生类和重写成员继承,则为 true,否则为 false。 默认值为 true ”
如下,我们设置 Inherited = false 那么继承TClass的T2Class无法访问到TClass中设置的特性元数据。
namespace net { [AttributeUsage(AttributeTargets.All, Inherited = false)]//3.设置可用于哪些对象 public class TMessgAttribute : Attribute//1.定义类TMessg加上后缀TMessgAttribute 2.继承Attribute。 { public TMessgAttribute() { } /// <param name="createTime">创建时间</param> /// <param name="createName">创建人</param> public TMessgAttribute(string createTime, string createName, string mess) { this._createName = createName; this._createTime = createTime; this._mess = mess; } private string _createTime; public string createTime { get { return _createTime; }//4.只能有get方法 } private string _createName; public string createName { get { return _createName; } } private string _mess; public string mess { get { return _mess; } } /// <summary> /// 修改时间 /// </summary> public string modifyTime { get; set; } } class Program { static void Main(string[] args) { System.Reflection.MemberInfo info = typeof(T2Class); //通过反射得到TClass类的信息 TMessgAttribute hobbyAttr = (TMessgAttribute)Attribute.GetCustomAttribute(info, typeof(TMessgAttribute)); Console.WriteLine("类名:{0}", info.Name); if (hobbyAttr != null) { Console.WriteLine("创建时间:{0}", hobbyAttr.createTime); Console.WriteLine("创建人:{0}", hobbyAttr.createName); Console.WriteLine("备注消息:{0}", hobbyAttr.mess); Console.WriteLine("修改时间:{0}", hobbyAttr.modifyTime); } Console.ReadKey(); } } [TMessg("2015-12-20", "zhaopei", "我只是测试自定义特性,不要报错哦,求求你了。", modifyTime = "2015-12-21")] public class TClass { //................ } public class T2Class : TClass { //........... } } View Code
反之,我们设置 Inherited = true ( 或者不设置任何,因为默认就是true )打印如下:
3.AllowMultiple (也是一个布尔值) :“ 如果允许指定多个实例,则为 true;否则为 false。 默认值为 false。 ”
我们设置两个特性试试,如:
如果我们想要这样设置怎么办。在AttributeUsage中设置 AllowMultiple = true 如:
那么上面报错的地方将会打印:
注意:上面的打印地方的代码需要修改。因为之前是打印一个特性信息,这里是打印一个特性数组集合的信息。
static void Main(string[] args) { System.Reflection.MemberInfo info = typeof(T2Class); TMessgAttribute[] hobbyAttr = (TMessgAttribute[])Attribute.GetCustomAttributes(info, typeof(TMessgAttribute));//修改1.这里需要取特性数据的集合了 Console.WriteLine("类名:{0}", info.Name); for (int i = 0; i < hobbyAttr.Count(); i++)//修改2.这里需要循环打印了 { Console.WriteLine("================================================"); Console.WriteLine("创建人:{0}", hobbyAttr[i].createName); Console.WriteLine("创建时间:{0}", hobbyAttr[i].createTime); Console.WriteLine("备注消息:{0}", hobbyAttr[i].mess); Console.WriteLine("修改时间:{0}", hobbyAttr[i].modifyTime); } Console.ReadKey();
全部代码:
using System; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; namespace net { [AttributeUsage(AttributeTargets.All, Inherited = true, AllowMultiple = true)]//3.设置可用于哪些对象 public class TMessgAttribute : Attribute//1.定义类TMessg加上后缀TMessgAttribute 2.继承Attribute。 { public TMessgAttribute() { } /// <param name="createTime">创建时间</param> /// <param name="createName">创建人</param> public TMessgAttribute(string createTime, string createName, string mess) { this._createName = createName; this._createTime = createTime; this._mess = mess; } private string _createTime; public string createTime { get { return _createTime; }//4.只能有get方法 } private string _createName; public string createName { get { return _createName; } } private string _mess; public string mess { get { return _mess; } } /// <summary> /// 修改时间 /// </summary> public string modifyTime { get; set; } } class Program { static void Main(string[] args) { System.Reflection.MemberInfo info = typeof(T2Class); TMessgAttribute[] hobbyAttr = (TMessgAttribute[])Attribute.GetCustomAttributes(info, typeof(TMessgAttribute));//修改1.这里需要取特性数据的集合了 Console.WriteLine("类名:{0}", info.Name); for (int i = 0; i < hobbyAttr.Count(); i++)//修改2.这里需要循环打印了 { Console.WriteLine("================================================"); Console.WriteLine("创建人:{0}", hobbyAttr[i].createName); Console.WriteLine("创建时间:{0}", hobbyAttr[i].createTime); Console.WriteLine("备注消息:{0}", hobbyAttr[i].mess); Console.WriteLine("修改时间:{0}", hobbyAttr[i].modifyTime); } Console.ReadKey(); } } [TMessg("2015-12-20", "zhaopei", "我只是测试自定义特性,不要报错哦,求求你了。", modifyTime = "2015-12-21")] [TMessg("2015-12-21", "zhaopei", "我再次测试,还能给我面子显示出来吗?", modifyTime = "2015-12-22")] public class TClass { //................ } public class T2Class : TClass { //........... } } View Code
上面我们通过反编译,发现自定义特性实际上就是一个对象调用的最前面加了一段实例化的代码。
那么我们可以做的事可多了,除了像上面一样为对象设置“注释”,我们还可以自定义个特性,给某些方法或是某些类做“操作日志记录”,或者给需要在执行某些方法的时候需要权限,我们可以做个权限认证的特性等等。
这里就需要大家自己去扩展了。