[C#1] 12-特性
1.特性简介
特性仅仅是为目标元素提供关联的附加信息的一种方式,编译器的工作只是将这些附加信息放到托管模块的元数据中, 大多特性对于编译器来说没有任何特殊的意义,编译器只是检测源代码中的定制特性,然后产生相应的元数据。 FCL中已经带来了很多的预定义特性[System.ParamArrayAttribute,SerializableAtribute...]。 在C#中,将特性放在紧挨着目标元素前的一个方括号[]中,就表示该元素应用该特性了。 CLR允许将特性应用于任何可以在一个文件的元数据中表示的元素上。 元数据的定义表中的条目[TypeDef,MethodDef,ParamDef];元数据引用表中的条目[AssemblyRef,ModuleRef,TypeRef,MemberRef], 虽然CLR允许应用在任何上述这些条目,但是大多数语言只允许应用在元数据定义表的条目上,C#便是如此, C#允许在[程序集,模块,类型,字段,方法,方法参数,方法返回值,属性,事件]上应用特性。
特性实质是一个类型的实例,要与CLS兼容,定制的特性必须直接或者间接的继承自System.Attribute。C#只允许使用与CLS兼容的特性。 一个特性就是一个类型的实例,该类型必须有一个共有构造器来创建它的实例,所以我们在目标元素上应用一个定制特性时, 其语法类似于调用类型的实例构造器,一些语言还允许使用一些特殊的语法来设置特性类型的公有属性或或者字段。
2.定义自己的特性
1 //根据约定,所有特性类型后缀都是<Attribute> 2 //所有非抽象特性也必须至少包含一个公有构造器 3 //现在这个特性就可以用在任何目标元素上了,假如 4 //要显示的控制其应用的目标元素类型,则可以使用 5 //System.AttributeUsage特性,假如不应用此特性 6 //C#编译器和CLR将默认设置为可以应用在所有元素 7 //一个目标元素只可用一次,可以被继承,实际上是 8 //AttributeUsagez中字段的默认值 9 10 //AttributeTargets.Method表示只可用在方法上 11 //Inherited属性表示是否将特性应用与派生类或派生方法上 12 [AttributeUsage(AttributeTargets.Method, Inherited = false)] 13 public class TempAttribute : Attribute 14 { 15 private string name = string.Empty; 16 public TempAttribute(string name) 17 { 18 this.name = name; 19 } 20 public string Time { get; set; } 21 public string Name 22 { 23 get 24 { 25 return this.name; 26 } 27 } 28 }
特性类型的实例构造器,字段,属性被严格的定义在一个很小的子集中[Boolean,Char,Byte,SByte,Int16,UInt16,Int32,UInt32,Int64,UInt64,Single,Double,String,Type,Object]或者枚举类型, 或者这些类型的一维0基数组。应用特性:
1 //[Temp("乱舞春秋", Time = "2011-4-9")] 2 //在类上声明此属性则会引起编译报错,因为我们只允许它应用在方法上 3 //属性“Temp”在该声明类型中无效。它只在“method”声明中有效。 4 public class Test 5 { 6 static void Main() 7 { 8 Show(); 9 } 10 [Temp("乱舞春秋",Time="2011-4-9")] 11 public static void Show() 12 { 13 Console.WriteLine("输出点..."); 14 } 15 }
使用的语法有些奇特,我们的自定义特性的公有实例构造器只接受一个string类型的参数。 "乱舞春秋"就被这样传递过去了[构造器参数成为定位参数,该参数是强制性的,必须指定的], 那么后面的Time呢,它是自定义特性的一个共有字段,这类"参数"成为命名参数,并且是可选的。看IL代码:
1 .method public hidebysig static void Show() cil managed 2 { 3 //在Show内部调用了TempAttribute的构造函数,并初始化了Time公有字段 4 .custom instance void TempAttribute::.ctor(string) = 5 ( 01 00 0C E4 B9 B1 E8 88 9E E6 98 A5 E7 A7 8B 01 6 54 0E 04 54 69 6D 65 08 32 30 31 31 2D 34 2D 39 ) // .T..Time.2011-4-// 9 7 8 // 代码大小 13 (0xd) 9 .maxstack 8 10 IL_0000: nop 11 IL_0001: ldstr bytearray (93 8F FA 51 B9 70 2E 00 2E 00 2E 00 ) // ...Q.p...... 12 IL_0006: call void [mscorlib]System.Console::WriteLine(string) 13 IL_000b: nop 14 IL_000c: ret 15 } // end of method Test::Show
3.检测特性
FCL提供了许多检测特性是否存在的方式,如果使用System.Type对象检测特性,则可以使用IsDefined方法检测指定特性是否存在。有时候目标元素不是一个类型,而是一个程序集,一个模块,一个方法,或者一个字段,这要用System.Attribute提供的方法。
方法 | 描述 |
---|---|
IsDefined | 如果至少有一个指定的Attribute的派生类型应用在目标元素上,就返回true,该方法速度较快<不需要反序列化任何特性类型的实例> |
GetCustomAttributes | 返回一个Attribute数组,其中的元素是指定的、应用在目标元素上的特性实例。每一个实例都会用编译时指定的参数、字段、属性来构造<反序列化>,如果目标元素没有应用任何特性则返回一个空数组。通常用在那些将AllowMultiple为true的特性 |
GetCustomAttribute | 返回一个指定的、应用在目标元素上的特性实例,同样会反序列化,如果没有返回null,如果应用了多个特性则抛出System.Reflection.AmbiguousMatchException异常,通常用在那些将AllowMultiple为fasle的特性 |
检测特性信息:
System.Reflection.MethodInfo myShow = (typeof(Test)).GetMethod("Show"); bool isHaveTempAttribute = Attribute.IsDefined( myShow, typeof(TempAttribute), false); Console.WriteLine(isHaveTempAttribute);//输出true TempAttribute myTemp = Attribute.GetCustomAttribute( myShow, typeof(TempAttribute)) as TempAttribute; if (myTemp != null) { Console.WriteLine(myTemp.Name);//输出乱舞春秋 Console.WriteLine(myTemp.Time);//输出2011-4-9 }
另外System.Reflection命名空间写的一些类型可以使我们利用反射获得元数据内容, 这些类型也都提供有IsDefined和GetCustomAttributes方法[返回一个object类型额数组,而不是Attribute类型的, 这是因为这些反射类型可以返回与CLS不兼容的特性类型的实例],但是没有GetCustomAttribute方法。
4.特性实例间的匹配
在检测特性中我们取到了特性的对象,看到了其中的数据。我们也可以通过重写System.Attribute的虚方法Match, 然后在代码中构造一个特性实例,并调用该方法去比较检测出的特性实例。在TemoAttribute特性类中加上如下方法:
//只比较Time属性和Name属性,写的比较简单 public override bool Match(object obj) { bool isTempType = obj is TempAttribute; if (!isTempType) { return false; } TempAttribute newTemp = (TempAttribute)obj; //如果这两个字段都相等就返回ture if (this.name==newTemp.name&&this.Time==newTemp.Time) { return true; } return false; }
测试下:
//myTemp用的是上面 Attribute.GetCustomAttribute得到的特性实例 TempAttribute newTemp = new TempAttribute("乱舞春秋"); bool isEqual = myTemp.Match(newTemp); //输出false,没有赋值Time属性嘛 Console.WriteLine(isEqual); newTemp.Time = "2011-4-9"; isEqual = myTemp.Match(newTemp); //输出ture了 Console.WriteLine(isEqual);
5.伪特性
微软定义的某些特性使用的非常频繁,如果这些特性信息都存放到元数据中将导致托管模块的大小急剧膨胀, 因此这些特性都在编译时经过了特殊的处理,它们将以位的形式[高度压缩]存放在元数据中。 CLR和FCL也知道怎样在元数据中以一种特殊的方式查找这些伪定制特性。
关于伪定制特性最重要的一点是我们不能在运行时像检测普通的特性那样的方式来检测它们是否存在,只能采用一些特殊的方法来检测, 例如System.Type提供的只读属性[IsSerializable,IsAutoLayout...]。【注,这是.NET框架2.0出现之前的书中解释】