前言:
本来打算将特性(Attribute)和反射(Reflection)写在一章里,但感觉反射(Reflection)的内容稍微有点点长,所以新起一章,从学习顺序方面,也是先学反射(Reflection)后学特性(Attribute)。
(官方文档:https://docs.microsoft.com/zh-cn/dotnet/api/system.type?view=netframework-4.7.2)
概念:
一、什么是特性(Attribute)?
CLR(common language runtime)允许你向程序的元素(programming elements)
(types(classes),结构structs,字段fields,方法methods,属性properties,委托delegates,事件Events,方法参数Params,方法返回值ReturnValue,泛型generic<xxx>参数)
添加一些描述性的信息,这些特性数据会和元数据(metadata)存放在一起,并通过反射(Reflection)技术运行时获取。
添加这些Attribute属性的目的是实现一些丰富的功能,比如在设计和调试阶段使用Attribute特性来辅助开发。
我们在目标元素上应用特性Attribute,会改变目标的行为。比如FlagsAttribute,限定用于System.Enum枚举类型,当在Enum上应用FlagsAttribute后,在运行时,会通过反射Reflection检测Enum是否使用了FlagsAttribute,如果使用,则会改变Enum的ToString和Format的行为.如果没有,就视为一个普通的枚举类型。
注:元数据(metadata)是用表来存储的,程序集中会定义多个表,如类型定义表(types table),字段定义表(fields table),方法定义表(methods table),属性定义表(properties table)等等
.net framework出于各种原因,可以通过特性解决许多的问题。
比如如何序列化和反序列化一个类或是字段[Serializable][NonSerialized],
提示用户某些方法已经或是将要被废弃[Obsolete],
某些方法需要通过非拖管的代码来实现[DllImport],
或是提供一些便利的操作...
二、常用的特性有哪些?
[Serializable]//序列化和反序列化,可以添加到类Class,结构Struct,枚举Enum,委托Delegate上,该特性不具有继承性
[NonSerialized]//声明某个字段Field不需要进行序列化处理,也不具有继承性,这通常是某些需要动态进行计算的值,比如说角色的总战力
[DllImport]//声明该方法使用非拖管代码来实现,比如Unity3D开发当中,与iOS进行交互,调用iOS的API时,就需要使用该特性
[Obsolete]//声明某个程序元素(program elements)将不会再使用了,准备废弃了,可以使用在某何程序元素上.
[Flags]//声明枚举被当做一个位字段来处理(bit field),具体FlagsAttribute的解释请看前面的文章”FlagsAttribute是什么?"
[AttributeUsage(AttributeTargets.Enum,Inherited=false)]//限制特定只能用于枚举Enum上,并且不具有继承性
[ParamArrayAttribute]//不定参数实际是一种特性Attribute,void test(params string[] val){...}
以上都是CLR预定义的特性,可以根据需求自己实现"自定义"CustomAttribute特性.
三、特性Attribute的使用
特性Attribute实际上是一个类的实例,必须直接或间接继承自System.Attribute抽象类,CLR预定义的特性Attribute使用时,大多数特性都需要引入命名空间:
using System.Runtime.InteropServices;
特性可以定义在任意的命名空间中.
将特性Attribute应用于目标元素时,相当于调用了该特性Attribute类的实例构造函数,下面是一些在目标元素应用特性Attribute的例子:
[Serializable]//目标Person为可序列化和反序列化的类
public class Person{
//...........
}
[NonSerialized]//字段name 不进行序列化和反序列化
public string name;//Field
[OptionalField]//指定name字段为可选序列化,这通常用于可序列化的类中,新增成员时,要指定新增的成员是禁止序列化还是可选序列化,如果不添加,那么旧的序列化数据在进行反序列化时,会抛出异常,name无法被反序列化
public string name;//Field
[Flags]//在枚举Vegetables上应用Flags属性,改变枚举的ToString和Format方法的行为
public enum Vegetables
{
Cabbage = 1<<0,
Carrot = 1<<1,
Cuke = 1<<2,
Potato = 1<<3,
}
[DllImport("__Internal")]//在StartPayment方法上应用DllImport,该方法由非拖管代码实现,如Unity C#与iOS交互
public static extern void StartPayment(string paymentId,int bonusType);
在使用特性Attribute时,我们可以添加一个前缀来指定要应用于的目标元素,许多时候,即使是省略前缀,编译器也能判断特性要应用于什么目标元素,如:
[type:Serializable]
public class Person{
//...
}
[method:Obsolete]
public Person(string name,int age,bool married,float deposit)
{...}
[property:someAttr]
public string plan { ... }
[return:someAttr]
private string MarriedState(){...}
public static void SaySomething([param:someAttr] string words)
特性Attribute在使用时,可以省略后面的Attribute,减少代码量,提高可读性,比如[Serializable]的全称是[SerializableAttribute]
[Flags]=[FlagsAttribute],但在使用中,我们不需要加上Attribute后缀,在自定义特性的时候,我们需要加上,统一规范。
前面提到过,特性Attribute实际上是类的实例,我们在使用时,传递的参数,是调用的该特性公共的构造函数,在特性中Attribute中有两种参数:
1.定位参数(positional parameter),又叫必要参数,即我们必须要传递的参数,和调用常规的函数没有区别,按顺序类型传递需要的参数即可,如:
[DllImport("__internal")] //DllImportAttribute特性类提供了一个接受string参数的构造函数,传递了字符串__internal就是定位参数
2.命名参数(named parameter),又称为可选参数,他不是必需要设置的,通过命名参数来为特性Attribute类中的公共字段或属性进行赋值,如:
[DllImport("__internal"),CharSet=CharSet.Auto,SetLastError=true]
//CharSet和SetLastError是公共的实例字段或属性,通过命名参数来赋值
其它的,可以在目标元素上同时应用多个特性,他们可分别在单独的[](square bracket)中,也可放在一个[](square bracket)中,并以逗号,(comma)分离,并且和顺序无关,以下几个都是等价的(只是演示不同的声明形式,不考虑可行性):
[Obsolete][Serializable][Flags]
[Obsolete,Flags,Serializable]
[FlagsAttribute()][ObsoleteAttribute()][SerializableAttribute()]
[FlagsAttribute][ObsoleteAttribute][SerializableAttribute]
四、定义自己的特性Attribute类
首先,定义自己的特性Attribute类,必须要直接或间接的从System.Attribute抽象类派生,并且至少要包含一个公共的构造函数,虽然特性类型是一个类,但这个类非常的简单,除了提供公共的字段和属性,他不应该提供更多的公共方法,事件或其它的成员。通常建议使用属性,而不是字段,便于修改。
以FlagsAttribute特性为例:
namespace System.test{
public class FlagsAttribute:System.Attribute{//派生自抽像类System.Attribute
//至少提供一个公共的构造函数
public FlagsAttribute()
{
}
}
}
有了特性Attribute类以后,我们通常要限定特性的使用范围,即可以在哪些目标元素上使用,这就需要使用预定义的特性System.AttributeUsageAttribute。
这样我们限制System.test.FlagsAttribute只能应用于枚举Enum:
namespace System.test{
[AttributeUsage(AttributeTargets.Enum,Inherited=false)]
public class FlagsAttribute:System.Attribute{//派生自抽像类System.Attribute
//至少提供一个公共的构造函数
public FlagsAttribute()
{
}
}
}
当你在types,field,method,properties,events,delegates,returnvalue,params...上使用时,会出现编译错误。
AttributeUsage特性类很简单,他提供了三个公共属性,分别是ValidOn,Inherited和AllowMultiple,ValidOn只能get,通过公共的构造函数来set设置ValidOn,一个位标志AttributeTargets.
ValidOn:应用在哪些目标元素上,可以通过位|(OR)组合,应用于多个目标元素。
AttributeTargets在FCL中的定义请查看官方文档:
https://docs.microsoft.com/en-us/dotnet/api/system.attributetargets?view=netframework-4.7.2
Inherited:是否具有继承性,比如你应用在类上,如果Inherited=false,那么基类使用了该特性,派生类是不会继承该特性的,比如说[Serializable]
[AttributeUsage(AttributeTargets.Class|AttributeTargets.Method,Inherited=true)]
public class StrongAttribute:System.Attribute{
public StrongAttribute()
{}
}
[Strong,Serializable]
public class BaseClass{
[Strong("Base")]
public virtual void DoSomething(string v)
{}
}
public class DerivedClass:BaseClass
{
public override void DoSomething(string v)
{}
}
BaseClass应用了[Strong,Serializable]特性,DerivedClass派生自BaseClass,所以DerivedClass也继承[Strong]特性,因为Inherited=true,但Derived并不能够被序列化和反序列化,因为Serializable特性类中Inherited=false
AllowMultiple:是否允许将特性多次应用于同一个目标,通常来说是没有意义的,比如[Flags][Flags],重复定义,没有实际作用,但有些特性还是需要的,比如条件特性类.
[Conditional("DEBUG")][Conditional("SANDBOX")]
只有在定义了DEBUG或SANDBOX符号的前提下,编译器才会在元数据中生成特性信息。这个在设计和调试阶段,那些用于辅助开发的特性非常有帮助,运行中不需要的特性就不要加到元数据了,减少程序集的大小。
最后,如果没有指定AttributeUsage特性,那么会使用默认值,即可以使用在所有的目标元素,并且Inherited=true
五、检测定制的特性Attribute
我自定义了特性Attribute并应用在目标元素上以后,我在运行时,要进行检测是否使用了该特性,并执行逻辑分支。
比如枚举应用了Flags特性,那么我在调用ToString的时候,就要检测当前的枚举是否应用了Flags特性,如:
public override string ToString ()
{
//检测枚举类型是否应用了FlagsAttribute特性
if (this.GetType ().IsDefined (typeof(FlagsAttribute), false)) {
//接位标志,以字符串的形式输出
} else {
//当成一个普通的枚举值处理
}
}
通过调用Type的IsDefined方法,返回true,表明该目标元素应用了特性。isDefined的第二个参数是Inherited,是否检测特性的派生类,如果你只想检测指定的类,那么额外的检查是没有必要的,可以将特性类设置为sealed密封类。
如果只是想检测是否应用了某个特性,使用IsDefined就可以了,很高效,不生成实例,但特性中我们可能会传递一些参数,想要获取这些特性的参数,就需要使用另外两个静态方法:
1.GetCustomAttributes()//返回目标元素上应用的所有特性,通常应用于将AllowMultiple设置为true的特性,一个特性多次用于同一个目标元素上。
2.GetCustomAttribute()//返回目标元素上应用的指定的特性。
Retrieves a custom attribute of a specified type applied to an assembly, module, type member, or method parameter.
可以用于参数parameter和module以及assembly.
(文档地址:https://docs.microsoft.com/en-us/dotnet/api/system.attribute.getcustomattribute?view=netframework-4.7.2)
获取特性的实例这些操作是比较消耗的,从性能角度,可以缓存这些方法的返回结果。
下面实现一个小例子,现应用GetCustomAttributes和GetCustomAttribute方法。(来自于官方DEMO)
// Define a custom parameter attribute that takes a single message argument.
[AttributeUsage( AttributeTargets.Parameter )]
public class ArgumentUsageAttribute : Attribute
{
// This is the attribute constructor.
public ArgumentUsageAttribute( string UsageMsg )
{
this.usageMsg = UsageMsg;
}
// usageMsg is storage for the attribute message.
protected string usageMsg;
// This is the Message property for the attribute.
public string Message
{
get { return usageMsg; }
set { usageMsg = value; }
}
}
public class BaseClass
{
// Assign an ArgumentUsage attribute to the strArray parameter.
// Assign a ParamArray attribute to strList using the params keyword.
public virtual void TestMethod(
[ArgumentUsage("Must pass an array here.")]
String[] strArray,
params String[] strList)
{ }
}
public class DerivedClass : BaseClass
{
// Assign an ArgumentUsage attribute to the strList parameter.
// Assign a ParamArray attribute to strList using the params keyword.
public override void TestMethod(
String[] strArray,
[ArgumentUsage("Can pass a parameter list or array here.")]
params String[] strList)
{ }
}
public void test()
{
Type t = typeof(DerivedClass);
MethodInfo info = t.GetMethod ("TestMethod");
ParameterInfo[] pInfoArray = info.GetParameters();
foreach (var p in pInfoArray) {
if (Attribute.IsDefined (p, typeof(ArgumentUsageAttribute))) {
ArgumentUsageAttribute usageAttr = (ArgumentUsageAttribute)
Attribute.GetCustomAttribute(
p, typeof(ArgumentUsageAttribute) );
if (usageAttr != null) {
Debug.Log ("Usage:"+usageAttr.Message);
if (!p.ParameterType.IsArray) {
Debug.LogError ("You must set parameter to Array!");
}
}
}
}
}
创建了一个特性Attribute类ArgumentUsage,默认可继承
又创建了一个BaseClass和DerivedClass派生类,
在基类的virtual方法中第一个参数加上特性 [ArgumentUsage("Must pass an array here.")],
因为ArgumentUsage具有继承性,所以派生类DerivedClass中重载的方法的参数,
也继承[ArgumentUsage("Must pass an array here.")],在派生类DerivedClass中又加入了 [ArgumentUsage("Can pass a parameter list or array here.")]特性,
那么在test2方法中,我通过反射Reflection来获取方法TestMethod,并获取参数是否应用ArgumentUsage特性。
答案是Must pass an array here和Can pass a parameter list or array here都会输出。
下面加了一条判断if (!p.ParameterType.IsArray),判断参数如果不是数组,则抛出异常。
(GetCustomAttributes的例子就不列了,返回目标元素上的使用的特性数组。)
注:这里实测时在unity上有问题,在VS IDE上测试是正常通过,在unity下测试Derived中的string[] strArray并没有继承自父类BaseClass的参数的特性,初步断定是framework的问题,毕竟u3d目前使用的mono版本还比较老,还存在一些问题,比如foreach,这个问题有知道的同学麻烦指点一下,稍后会将该问题抛到stackoverflow上。
(stackoverflow问题地址:https://stackoverflow.com/questions/51351402/c-sharp-attribute-could-not-inherited-in-parameter-of-method)
到此为止,如果大家发现有什么不对的地方,欢迎指正,共同提高,感谢您的阅读!
编辑于2018.7.15
--闲言碎语
今天是2018年的7月15日,世界杯决赛的日子,早上曼尼帕奎奥在第七回合TKO了对手,酣畅淋漓,晚上的世界杯决赛我支持克罗地亚,世界杯让我对莫德里奇有了新的认识,开始我是支持Brazil,支持保利尼奥,支持库蒂尼奥,支持马塞洛,可惜巴西在对阵比利时的比赛中,发挥得并不好,裁判也有争议性的判罚,很遗憾,法国一直踢得比较顺利,所以希望今天克罗地亚可以给法国多制造一些麻烦,我希望莫德里奇拿到金球奖!