1.什么是Attribute
特性简单点理解就是为目标元素添加一些附加信息,这些附加信息我们可以在运行期间以反射的方式拿到。目标元素指的是程序集、模块、类、参数、属性等元素,附加信息指的是特性类中的成员。可以看出特性类其实就是一个数据结构,我们可以将各种各样的信息放入这个类中,并将特性类关联到指定目标元素中,在目标元素中每关联一个特性就创建一个特性类的实例,当然它的作用还不止如此。下面是使用特性的3段代码,分别是3个类。第一个是MyAttribute特性类,第二个是与特性类关联的目标类MyClass,第3个类是主程序类Program。在第一个类中,我使用了AttributeUsage系统特性类,.net还为我们提供了很多固定特性类,比如Serializable、Conditional、DllImport、Flags等。AttributeUsage类的参数作用我代码里已经有了注释,可以添加这个特性也可以不添加,但这个特性类在我这个程序中必须添加。原因是我在第二个类MyClass中添加了4个MyAttribute特性实例,而默认情况下只允许添加一个实例。因此我得在AttributeUsage中指定AllowMultiple为true,如果不指定编译会报错。
在AttributeUsage特性类的参数中,并不是完全以传值的形式创建的实例,其中还可以直接给参数赋值比如AllowMultiple和Inherited。对于前者一般是构造函数中的参数,我们必须给构造函数赋值否则就不能初始化了,因此这种类型的参数是固定参数。对于后者则是可选参数,细心点会发现它就是特性类中的属性,比如在第二个类MyClass中就给Hobby属性赋值了。正因为参数里有可选参数,故MyClass可以同时关联4个特性实例。在第三个类中,我通过GetCustomAttribute拿到定制的特性数组,其实就是MyAttribute的实例,这样就可以通过实例对象获取里面的数据成员了。这便是Attribute的基本使用,由于使用时需要调用构造函数,因此定制特性类必须有公共构造函数。将程序的exe文件放入Reflector中可以很清楚的看到特性就是一个类,使用特性其实就是调用特性的构造函数。
//特性也可以用在特性上 [AttributeUsage( AttributeTargets.All, //目标元素可以是任何元素 AllowMultiple=true, //多个特性可以加在一个元素上,为false时一个元素上只允许有这个特性的唯一实例 Inherited=true)] //特性可被子类继承,为false时不可以被继承 class MyAttribute : Attribute { //字段 public string name="默认名字"; public int age = 0; string hobby="默认爱好"; //属性 public string Hobby { get { return hobby; } set { hobby = value; } } //构造方法 public MyAttribute() { } public MyAttribute(string name, int age) { this.name = name; this.age = age; } //实例方法 public void haha() { Console.WriteLine("哈哈"); } }
[My()] [My(Hobby="足球")] [My("小方",20)] [My("小白",30,Hobby="篮球")] class MyClass { public void haha() { Console.WriteLine("我是MyClass类"); } }
class Program { static void Main(string[] args) { /* * 本来想看一下会不会默认拿第一个实例,结果执行时报错:找到同一个类型的多个实例 //拿到MyClass上的一个特性实例 MyAttribute myAttribute = (MyAttribute)Attribute.GetCustomAttribute(typeof(MyClass), typeof(MyAttribute)); //看看这个实例是哪一个 if (myAttribute!=null) { Console.WriteLine(myAttribute.name); Console.WriteLine(myAttribute.age); Console.WriteLine(myAttribute.Hobby); myAttribute.haha(); } */ //拿到MyClass上的特性实例数组,这里有4个MyAttribute的实例 MyAttribute[] myAttributes = (MyAttribute[])Attribute.GetCustomAttributes(typeof(MyClass), typeof(MyAttribute)); MyAttribute myAttribute = myAttributes[0]; if (myAttribute != null) { Console.WriteLine(myAttribute.name); Console.WriteLine(myAttribute.age); Console.WriteLine(myAttribute.Hobby); myAttribute.haha(); } Console.ReadLine(); } } /*执行结果: 小白 30 篮球 哈哈 */
2.Attribute的作用
因为特性的存在,让我们可以在程序运行时得到一些信息,再根据这些信息进行逻辑判断。比如可以使用特性来确保Model对象的数据全部去除空字符串,代码如下面所示。第一段代码指特性类MyAttribute,第二段代码指使用特性的MyClass类,第三段代码指MyClass类的扩展方法Trim,第四段代码指主程序类Program。在main方法中创建了一个myclass对象,在给这个对象赋值时特意指定了一些空格。假设现在需要将MyClass这个类作为数据库实体类People,它的实例存放着输入的数据,这个数据可能有空格。一般情况下我得调用trim()方法来去除空格,但是如果MyClass的属性很多的话那就很麻烦了,需要写很多ToString().Trim()方法。而使用特性+扩展方法则可以轻松很多,对于需要进行空格去除的属性添加一个MyAttribute特性,接着调用实例对象的Trim扩展方法。在Trim方法中,会遍历这个对象的所有属性,接着遍历每个属性的所有特性,并找到打了MyAttribute特性的属性,接着进行ToString().Trim()方法的调用并重新给属性赋值,这样只需写一句myclass.Trim()就可以实现除掉空格的功能。如果没有特性,虽然一样可以使用扩展方法来对属性进行去除空格,但是我们无法对指定的属性进行去除,只能一口气把所有类型为string的字符串空格全都去除。
[AttributeUsage(AttributeTargets.Property,Inherited=false,AllowMultiple=false)] public class TrimAttribute : Attribute { //字段与属性 readonly Type myType; public Type MyType { get { return this.myType; } } //构造函数 public TrimAttribute(Type type) { myType = type; } }
class MyClass { [TrimAttribute(typeof(string))] public string Name { get; set; } [TrimAttribute(typeof(string))] public string Hobby { get; set; } [TrimAttribute(typeof(string))] public string Address { get; set; } }
//扩展方法必须是静态类,静态方法。 public static class TrimAttributeExtension { public static void Trim(this object obj) { Type t = obj.GetType(); //得到myclass实例对象的所有属性 foreach (PropertyInfo prop in t.GetProperties()) { //得到某个属性上的所有特性 foreach(var attr in prop.GetCustomAttributes(typeof(TrimAttribute),true)) { TrimAttribute trimAttribute = (TrimAttribute)attr; //获得obj的prop属性的值 object o=prop.GetValue(obj, null); //如果o不为null且这个属性上的特性实例的MyType属性是string类型 if (o!= null && (trimAttribute.MyType == typeof(string))) { //重新给这个属性赋值,也就是已经Trim()后的,可以看到GetPropertyValue(obj, prop.Name)其实就是o。 object newValue = GetPropertyValue(obj, prop.Name).ToString().Trim(); prop.SetValue(obj, newValue, null); } } } } //拿到属性本身所表示的值 private static object GetPropertyValue(object instance, string propertyName) { //首先得到instance的Type对象,然后调用InvokeMember方法, //这个方法的第一个参数意思是你需要调用的属性、方法、字段的”名字“,第二个参数是你调用propertyName是要干什么, //这里是拿到属性,第四个是要操作的实例。最后是需要传入的参数,这里调用属性因此不需要参数我就设置为null了。 return instance.GetType().InvokeMember(propertyName, BindingFlags.GetProperty, null, instance, null); } }
class Program { static void Main(string[] args) { MyClass myclass = new MyClass(); myclass.Name = "小方 "; myclass.Hobby = " 篮球 "; myclass.Address = " 湖北"; myclass.Trim(); /* 执行到这里会看到上面三个属性的值中空格全部都没有了 myclass.Name = "小方"; myclass.Hobby = "篮球"; myclass.Address = "湖北"; */ Console.ReadLine(); } }
3.总体上认识Attribute
特性,这个描述信息的数据类所描述的信息其实就是元数据。当我们在VS中生成解决方案时,在debug文件夹中就会出现一个exe文件,在windows中它称为可迁移可执行文件PE。PE由3部分组成:PE标头、IL、元数据。PE头主要作用是标识此文件是PE文件并说明在内存中执行程序的入口点。IL不用多说,但有一点要注意IL指令中常有元数据标记。元数据包含元数据表和堆数据结构。一个程序中会有很多类,这些类在PE中都会记录在一个记录类型的元数据表中,此外还有记录方法、字段等成员的元数据表,元数据表也可以引用其他的表和堆。可将这些表理解为数据库中的表,表之间通过主外键来建立一种约束与联系。不过我不知道这些表是如何创建的,是程序中某种成员的所有数据全部放在一起,还是有些数据比如字段是以类为划分的。元数据的堆数据结构有4种,分别是字符串、Blob、用户字符串、GUID。在IL中还有一个元数据标记,可以理解为一个指向元数据的指针,它包含4字节。第一个字节说明这个指针指向的类型,比如是指向类表呢还是指向方法表呢。后3个字节说明指向目标表中的位置,这种感觉有点像zigbee编程。再来看元数据的作用,在程序中定义的所有成员以及外部引入的成员都将在元数据中进行说明,这样在JIT生成机器指令时正是通过元数据中的信息来完成即时编译的。元数据中存储程序中程序集的说明(名称、版本、依赖的其他程序集等),类型的说明(类成员、可访问性、继承实现关系等),特性。到这里可以理解特性是属于PE中的元数据的一部分,具体到物理结构上我觉得是有一个元数据特性表,比如类型元数据表的一个类有一个指针指向它的元数据特性表,这个特性表记录着与这个类关联的所有特性。另外由于特性是作为元数据的一部分,因此特性类将会在编译时就实例化,而不是运行期动态实例化。