反射特性
假设我们在实现一个方法A,但是因为某种原因,这个方法A设计的不够好,我们需要重新设计一个方法B来代替方法A,因为很多客户程序中已经在使用旧版本的方法A了,如果简单地删除掉旧的方法A,使用老版本方法A的客户程序将无法工作,因此必须考虑兼容性。这个时候,我们应该同时保留A和B两个方法。那么我们会希望告知客户程序现在有一个全新的方法B可供使用,但此时客户程序并不知道已经存在一个新的方法B,那么又该如何做呢?
在.NET中可以使用特性来完成这一工作。特性是一种特殊的类型,可以标记到程序集或者程序集中的类型上,这些类型包括模块、类、接口、结构、构造函数、方法、方法参数等,标记了特性的类型称作特性的目标。特性是为程序集添加元数据(描述数据的数据)的一种机制,通过它可以为编译器提供指示或者对数据提供说明。
先通过一个实例来看一下特性是如何解决上面的问题的。我们可以给旧的A()方法加上Obsolete特性来告诉编译器这个方法已经过时,然后当编译器发现程序中有地方在使用以Obsolete标记过的方法时,就会给出一个警告信息。
定义一个TestClass进行测试:
public class TestClass
{
[Obsolete("Plese use B() instead of A()")]
public static void A()
{
Console.WriteLine("This is A.");
}
public static void B()
{
Console.WriteLine("This is B.");
}
}
class Program
{
static void Main(string[] args)
{
TestClass.A();
}
}
编译上面的代码,编译器会有条警告:
warning CS0618: 'TestClass.A()' is obsolete: 'Plese use B() instead of A()'
通过使用特性,可以看到编译器给出了警告信息,告诉客户程序存在一个新的方法可供使用,这样,程序员在看到这个警告信息后,便会考虑使用新方法了。
特性的使用方法
首先是有一对方括号“[]”,在左方括号“[”后紧跟特性的名称,比如Obsolete。随后是一个圆括号“()”,在这个圆括号中,不光可以传入构造函数的参数,还可以向特性的属性赋值。在Obsolete的例子中,仅传递了构造函数参数。
将光标移动到Obsolete上,然后按下F12转到Obsolete的定义,会发现它的全名是ObsoleteAttribute,继承自Attribute类。但是这里却仅用Obsolete来标记方法,这是.NET的一个约定,所有的特性应该均以Attribute来结尾,在为对象标记特性时,如果没有添加Attribute,编译器会自动寻找带有Attribute的版本。
在传入构造函数参数时,参数的顺序必须同构造函数声明时的顺序相同,所以在特性中也叫位置参数(Positional Parameters),与此相应,属性参数也叫做命名参数(Named Parameters)。
自定义特性
学习特性的最好办法就是自定义一个特性,然后使用它。
如何自定义特性:
1. 继承自Attribute类
2. 使用AttributeUsage特性标记自定义特性
看一下AttributeUsage的定义:
public sealed class AttributeUsageAttribute : Attribute {
public AttributeUsageAttribute(AttributeTargets validOn);
public bool AllowMultiple { get; set; }
public bool Inherited { get; set; }
public AttributeTargets ValidOn { get; }
}
由于特性本身就是用来描述类型的元数据,而这个特性又用来描述自定义的特性,因此可以认为它们是“元数据的元数据”(元元数据:meta-metadata)。
可以看到,它有一个构造函数,这个构造函数含有一个AttributeTargets类型的位置参数(Positional Parameter),还有两个命名参数(Named Parameter)。注意ValidOn属性不是一个命名参数,因为它不包含set访问器。
首先看上面AttributeUsage是如何加载到ObsoleteAttribute特性上面的:
[AttributeUsage(6140, Inherited = false)]
这里大家一定疑惑为什么会这样划分参数,这和特性的使用是相关的。假如AttributeUsageAttribute是一个普通的类,那么它的使用方式类似下面这样:
// 实例化一个 AttributeUsageAttribute 类
AttributeUsageAttribute usage=new AttributeUsageAttribute(AttributeTargets.Class);
usage.AllowMultiple = true; // 设置AllowMutiple属性
usage.Inherited = false; // 设置Inherited属性
但是,如果将特性只写成一行代码,然后紧靠在其所应用的目标类型上,那么怎么办呢?于是就采用了一种特殊的写法:不管是构造函数的参数还是属性,全部写到构造函数的圆括号中,对于构造函数的参数,必须采取构造函数参数的顺序和类型,因此叫做位置参数;对于属性,采用“属性=值”这样的格式,它们之间用逗号分隔,称作命名参数。于是上面的代码就减缩成了这样:
[AttributeUsage(AttributeTargets.Class, AllowMultiple=true, Inherited=false)]
可以看出,AttributeTargets.Class是构造函数参数(即位置参数),而AllowMutiple和Inherited是属性(即命名参数),命名参数是可选的。
AttributeUsage特性的构造函数接受一个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)]
AttributeUsage是一个位标记,可以使用或运算来进行组合,当这样写时:
[AttributeUsage(AttributeTargets.Class|AttributeTargets.Interface)]
表示既可以将特性应用到类上,也可以应用到接口上。而ObsoleteAttribute特性上加载的AttributeUsage是这样的:
[AttributeUsage(6140, Inherited = false)]
可以通过下面的语句来获得6140所包含的枚举项值:
Console.WriteLine((AttributeTargets)6140);
输出为:
//Class, Struct, Enum, Constructor, Method,
//Property, Field, Event, Interface, Delegate
AttributeUsage的Inherited和AllowMutiple属性
AllowMutiple属性用于设置该特性是不是可以重复地添加到一个类型上(默认为false)。
Inherited就更复杂一些了,如果将Inherited设置为True,当有一个类继承自特性标明的目标类时,目标类的子类也会获得该特性。而当将特性应用于一个方法,如果继承自该目标类的子类覆盖了这个方法,那么子类中的方法也将继承这个特性。
明白了这些设置以后,我们实现一个MyCustomAttribute:
[AttributeUsage(AttributeTargets.Class, AllowMultiple =true, Inherited = false)]
public class MyCustomAttribute: Attribute
{
public string Descr { get; }
public MyCustomAttribute(string descr)
{
Descr = descr;
}
}
创建好自定义特性以后,就可以在类上面进行标记了:
[MyCustom("Descr 1")]
[MyCustom("Descr 2")]
public class TestClass
{
[Obsolete("Plese use B() instead of A()")]
public static void A()
{
Console.WriteLine("This is A.");
}
public static void B()
{
Console.WriteLine("This is B.");
}
public override string ToString()
{
return "This is a test class";
}
}
运行下面的代码:
TestClass testClass = new TestClass();
Console.WriteLine(testClass.ToString());
这段程序简单地在屏幕上输出:
This is a test class
添加自定义的特性,只是完成了第一步,给程序集添加了自定义的元数据,如果我们不对自定义的元数据进行相关操作,自定义的元数据并没有什么用途。所以使用自定义特性的目的是使用反射查看自定义特性并在程序中的某处使用它。
我们现在完成第二步,通过反射对自定义特性进行访问:
Type type = typeof(TestClass);
MyCustomAttribute[] myCustomAttributes = type.GetCustomAttributes<MyCustomAttribute>().ToArray();
foreach (MyCustomAttribute myCustomAttribute in myCustomAttributes)
{
Console.WriteLine("CustomAttribute:{0}", myCustomAttribute);
Console.WriteLine("--Descr:{0}", myCustomAttribute.Descr);
}
输出为:
能够重新获取到特性对象后,就可以利用它做任何事情。