代码见文件:AttributeStudy.zip
特性无处不在,各种框架:MVC、IOC、ORM、WebService无处不在,常见的[Serializable]、[Controllers]等等
一、基本概念
可以理解成”标签“,给被标记的目标(class、property、field、method甚至方法的参数parameter)追加额外的信息。比如【不老男神】刘德华、【流量】xxx,对应到程序里的例子[Required]表示标记的字段非空、[MaxLength(32)]字段最大长度32位等等。
与注释不同,注释在编译的时候会被编译器(CLR)层丢弃,而特性不但不会被丢弃还会被编译进程序集程序集(Assembly)的元数据(Metadata)里,在程序运行时,从元数据提取出来决策程序进行。
1、它的本质是一个class(类),默认以Attribute结尾,直接或者是间接继承Attribute抽象类
2、应用特性,是把这个特性以[]标记在类或者是类内部成员上
3、标记的特性,如果是以Attribute结果,Attribute可以省略掉
4、可以标记的类内部的任何成员上
5、特性在标记的时候,其实就是去调用构造函数
6、在标记的时候也可以对公开的属性或者字段赋值
7、特性标记默认是不能重复标记的
二、具体应用
自定义一个特性CustomAttribute,继承Attribute。对应的约束:
AttributeTargets.All--可标记在类及类的成员上
AllowMultiple = true 允许重复标记
Inherited = true 允许被继承
/// <summary> /// AttributeUsage--也是一个特性,是用来修饰特性的特性--约束特性的特性 /// AttributeTargets--当前特性只能标记在某个地方;,我这个特性是专们用来标记在哪里的推荐使用(AttributeTargets.Class、AttributeTargets.Property......) /// AllowMultiple--是否允许重复标记 /// Inherited--是否允许被继承 /// </summary> [AttributeUsage(AttributeTargets.All,AllowMultiple = true,Inherited = true)] public class CustomAttribute:Attribute { private string _TypeDescription; public string _Name; /// <summary> /// 构造函数一 /// </summary> /// <param name="typeDescription"></param> public CustomAttribute(string typeDescription) { _TypeDescription = typeDescription; Console.WriteLine($"这是{_TypeDescription}"); } //构造函数二 public CustomAttribute(string name,string typeDescription) { _TypeDescription = typeDescription; _Name = name; Console.WriteLine($"这是{_TypeDescription}"); } }
实际调用
namespace AttributeStudy.Model { [Custom("学生")] [Custom("学生")] public class Student { /// <summary> /// 标记特性时实际是调用属性的构造方法 /// </summary> [Custom("学生姓名","中文名")] public string Name { get; set; } [Custom("学生年龄")] public int Age { get; set; } // _Name为publi类型,可在标记时赋值 [Custom("学生电话号码",_Name = "电话号码")] public string PhoneNumber { get; set; } [Custom("学生学习")] public void Study() { Console.WriteLine("学习"); } public void eat([Custom("食物")]string food) { Console.WriteLine($"吃{food}"); } } }
四、如何具体调用特性内部的成员
.class public auto ansi beforefieldinit AttributeStudy.Model.Student extends [System.Runtime]System.Object { .custom instance void AttributeStudy.CustomAttribute::.ctor(string) = ( 01 00 06 e5 ad a6 e7 94 9f 00 00 ) // Fields .field private string '<Name>k__BackingField' .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) .custom instance void [System.Runtime]System.Diagnostics.DebuggerBrowsableAttribute::.ctor(valuetype [System.Runtime]System.Diagnostics.DebuggerBrowsableState) = ( 01 00 00 00 00 00 00 00 ) .field private int32 '<Age>k__BackingField' .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) .custom instance void [System.Runtime]System.Diagnostics.DebuggerBrowsableAttribute::.ctor(valuetype [System.Runtime]System.Diagnostics.DebuggerBrowsableState) = ( 01 00 00 00 00 00 00 00 ) .field private string '<PhoneNumber>k__BackingField' .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) .custom instance void [System.Runtime]System.Diagnostics.DebuggerBrowsableAttribute::.ctor(valuetype [System.Runtime]System.Diagnostics.DebuggerBrowsableState) = ( 01 00 00 00 00 00 00 00 ) // Methods .method public hidebysig specialname instance string get_Name () cil managed { .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) // Method begins at RVA 0x208e // Code size 7 (0x7) .maxstack 8 IL_0000: ldarg.0 IL_0001: ldfld string AttributeStudy.Model.Student::'<Name>k__BackingField' IL_0006: ret } // end of method Student::get_Name .method public hidebysig specialname instance void set_Name ( string 'value' ) cil managed { .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) // Method begins at RVA 0x2096 // Code size 8 (0x8) .maxstack 8 IL_0000: ldarg.0 IL_0001: ldarg.1 IL_0002: stfld string AttributeStudy.Model.Student::'<Name>k__BackingField' IL_0007: ret } // end of method Student::set_Name .method public hidebysig specialname instance int32 get_Age () cil managed { .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) // Method begins at RVA 0x209f // Code size 7 (0x7) .maxstack 8 IL_0000: ldarg.0 IL_0001: ldfld int32 AttributeStudy.Model.Student::'<Age>k__BackingField' IL_0006: ret } // end of method Student::get_Age .method public hidebysig specialname instance void set_Age ( int32 'value' ) cil managed { .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) // Method begins at RVA 0x20a7 // Code size 8 (0x8) .maxstack 8 IL_0000: ldarg.0 IL_0001: ldarg.1 IL_0002: stfld int32 AttributeStudy.Model.Student::'<Age>k__BackingField' IL_0007: ret } // end of method Student::set_Age .method public hidebysig specialname instance string get_PhoneNumber () cil managed { .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) // Method begins at RVA 0x20b0 // Code size 7 (0x7) .maxstack 8 IL_0000: ldarg.0 IL_0001: ldfld string AttributeStudy.Model.Student::'<PhoneNumber>k__BackingField' IL_0006: ret } // end of method Student::get_PhoneNumber .method public hidebysig specialname instance void set_PhoneNumber ( string 'value' ) cil managed { .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) // Method begins at RVA 0x20b8 // Code size 8 (0x8) .maxstack 8 IL_0000: ldarg.0 IL_0001: ldarg.1 IL_0002: stfld string AttributeStudy.Model.Student::'<PhoneNumber>k__BackingField' IL_0007: ret } // end of method Student::set_PhoneNumber .method public hidebysig specialname rtspecialname instance void .ctor () cil managed { // Method begins at RVA 0x20c1 // Code size 8 (0x8) .maxstack 8 IL_0000: ldarg.0 IL_0001: call instance void [System.Runtime]System.Object::.ctor() IL_0006: nop IL_0007: ret } // end of method Student::.ctor // Properties .property instance string Name() { .custom instance void AttributeStudy.CustomAttribute::.ctor(string) = ( 01 00 0c e5 ad a6 e7 94 9f e5 a7 93 e5 90 8d 00 00 ) .get instance string AttributeStudy.Model.Student::get_Name() .set instance void AttributeStudy.Model.Student::set_Name(string) } .property instance int32 Age() { .custom instance void AttributeStudy.CustomAttribute::.ctor(string) = ( 01 00 0c e5 ad a6 e7 94 9f e5 b9 b4 e9 be 84 00 00 ) .get instance int32 AttributeStudy.Model.Student::get_Age() .set instance void AttributeStudy.Model.Student::set_Age(int32) } .property instance string PhoneNumber() { .custom instance void AttributeStudy.CustomAttribute::.ctor(string) = ( 01 00 12 e5 ad a6 e7 94 9f e7 94 b5 e8 af 9d e5 8f b7 e7 a0 81 00 00 ) .get instance string AttributeStudy.Model.Student::get_PhoneNumber() .set instance void AttributeStudy.Model.Student::set_PhoneNumber(string) } }
五、利用反射去获取、调用特性
在对Student进行反编译后,在内部可看到生成了Custom成员,但是不能直接调用。如果要调用需要用到反射。
思路:通过反射获取类及类成员的类型Type,再查看各个Type上有没有定义特性
定义一个工具类InvokeAttributeManager,访问类及类内部成员(属性、字段、方法)上的特性
GetCustomAttributes-返回使用定义在该成员变量上的特性数组,在这个步骤MyCustomAttribute的构造函数被调用
public class InvokeAttributeManager { /// <summary> /// 查看T实体类否有打上M特性 /// </summary> /// <typeparam name="T">对应的实体类</typeparam> /// <typeparam name="M">对应的标记</typeparam> /// <param name="t"></param> public static void ShowClassContainsAttribute<T, M>(T t) where M : Attribute { //获取实体类的类型 Type type = typeof(T); //1、判断T是否含有特性M //IsDefined--是否在该成员变量上定义了一个或多个attributeType实例 if (type.IsDefined(typeof(M), true)) { Console.WriteLine("类上定义了此特性"); //先判断是否含有此特性,再获取 //GetCustomAttributes-返回使用定义在该成员变量上的特性数组 //在这个步骤MyCustomAttribute的构造函数被调用 var attributes = type.GetCustomAttributes(true); foreach (M attribute in attributes) { Console.WriteLine($"包含特性名称:{typeof(M).Name}"); } } } /// <summary> /// 查看T实体类下的属性是否有打上M特性 /// </summary> /// <typeparam name="T"></typeparam> /// <typeparam name="M"></typeparam> /// <param name="t"></param> public static void ShowPropertyContainsAttribute<T, M>(T t) where M : Attribute { //获取实体类的类型 Type type = typeof(T); foreach (var propertyInfo in type.GetProperties()) { //1、判断propertyInfo是否含有特性M //IsDefined--是否在该成员变量上定义了一个或多个attributeType实例 if (propertyInfo.IsDefined(typeof(M), true)) { Console.WriteLine("属性上定义了此特性"); //先判断是否含有此特性,再获取 //GetCustomAttributes-返回使用定义在该成员变量上的特性数组 //在这个步骤MyCustomAttribute的构造函数被调用 var attributes = propertyInfo.GetCustomAttributes(true); foreach (M attribute in attributes) { Console.WriteLine($"包含特性名称:{typeof(M).Name}"); } } } } /// <summary> /// 查看T实体类下的字段是否有打上M属性 /// </summary> /// <typeparam name="T"></typeparam> /// <typeparam name="M"></typeparam> /// <param name="t"></param> public static void ShowFieldContainsAttribute<T, M>(T t) where M : Attribute { //获取实体类的类型 Type type = typeof(T); foreach (var fieldInfo in type.GetFields()) { //1、判断propertyInfo是否含有特性M //IsDefined--是否在该成员变量上定义了一个或多个attributeType实例 if (fieldInfo.IsDefined(typeof(M), true)) { Console.WriteLine("字段上定义了此特性"); //先判断是否含有此特性,再获取 //GetCustomAttributes-返回使用定义在该成员变量上的特性数组 //在这个步骤MyCustomAttribute的构造函数被调用 var attributes = fieldInfo.GetCustomAttributes(true); foreach (M attribute in attributes) { Console.WriteLine($"包含特性名称:{typeof(M).Name}"); } } } } /// <summary> /// 查看获取方法成员上的特性 /// </summary> /// <typeparam name="T"></typeparam> /// <typeparam name="M"></typeparam> /// <param name="t"></param> public static void ShowMethodsContainsAttribute<T, M>(T t) where M : Attribute { Type type = typeof(T); foreach (var methodInfo in type.GetMethods()) { //判断是否包含 if (methodInfo.IsDefined(typeof(M), true)) { var attributes = methodInfo.GetCustomAttributes(true); foreach (var attribute in attributes) { Console.WriteLine(attribute.GetType().Name); } } } } }
执行结果:
{ InvokeAttributeManager.ShowClassContainsAttribute<Student,MyCustomAttribute>(student); //结果打印: //类上定义了此特性 //这是学生1(在var attributes = type.GetCustomAttributes(true);时打印) //这是学生2(在var attributes = type.GetCustomAttributes(true);时打印) //包含特性名称:MyCustomAttribute //包含特性名称:MyCustomAttribute InvokeAttributeManager.ShowPropertyContainsAttribute<Student, MyCustomAttribute>(student); //打印: //属性上定义了此特性 // 这是中文名 //包含特性名称:MyCustomAttribute // 属性上定义了此特性 //这是学生年龄 // 包含特性名称:MyCustomAttribute // 属性上定义了此特性 //这是学生电话号码 // 包含特性名称:MyCustomAttribute InvokeAttributeManager.ShowFieldContainsAttribute<Student, MyCustomAttribute>(student); //字段上定义了此特性 // 这是所在学校 //包含特性名称:MyCustomAttribute InvokeAttributeManager.ShowMethodsContainsAttribute<Student, MyCustomAttribute>(student); //这是学生学习 //MyCustomAttribute }
六、特性的应用
1、利用特性去获取枚举的含义
避免每次获取含义时,都要调用如下一个层层if的方法去获取
public string getMean(enum enumA){ if(enumA == enum.A ){ return "正常"; } if(enumA == enum.B ){ return "冻结"; } 。。。。 }
定义一个说明特性RemarkAttribute,去说明enum中的字段含义
public class RemarkAttribute:Attribute
{
private string _remark;
public RemarkAttribute(string remark)
{
_remark = remark;
}
/// <summary>
/// 为外界暴露一个获取说明的方法
/// </summary>
/// <returns></returns>
public string GetRemark()
{
return _remark;
}
}
定义一个UserStateEnum,打上Remark特性
public enum UserStateEnum { [Remark("正常状态")] Normal = 1, [Remark("删除状态")] Deleted = -1, [Remark("冻结状态")] Frozen = 2, //没有状态 Empty = 0 }
定义一个InvokeRemarkManager工具类,定义一个enum的拓展方法去获取含义
public static class InvokeRemarkManager { /// <summary> /// 获取枚举字段对应的标记含义 /// </summary> /// <param name="eEnum"></param> /// <returns></returns> public static string getRemark(this Enum eEnum) { Type type = eEnum.GetType(); //获取枚举字段的名称 string fieldName = eEnum.ToString(); //获取对应的枚举字段 FieldInfo fieldInfo = type.GetField(fieldName); //获取枚举字段对应的解释说明 //判断该字段上是否定义了RemarkAttribute特性 if (fieldInfo.IsDefined(typeof(RemarkAttribute),true)) { //获取RemarkAttribute特性,两种写法 //RemarkAttribute attr = (RemarkAttribute)fieldInfo.GetCustomAttribute(typeof(RemarkAttribute)); RemarkAttribute attr = fieldInfo.GetCustomAttribute<RemarkAttribute>(); //获取RemarkAttribute的remark return attr.GetRemark(); } return "该枚举字段未创建说明"; } }
测试及结果打印:
//1、获取枚举变量的实际说明 { var remark1 = InvokeRemarkManager.getRemark(UserStateEnum.Deleted); var remark2 = InvokeRemarkManager.getRemark(UserStateEnum.Frozen); var remark3 = InvokeRemarkManager.getRemark(UserStateEnum.Empty); Console.WriteLine($"UserStateEnum.Deleted———{remark1}"); Console.WriteLine($"UserStateEnum.Frozen———{remark2}"); Console.WriteLine($"UserStateEnum.Empty———{remark3}"); //结果打印: //UserStateEnum.Deleted———删除状态 //UserStateEnum.Frozen———冻结状态 //UserStateEnum.Empty———该枚举字段未创建说明 }
2、利用特性去实现数据字段的校验(比如web api提交数据的场景)
模拟一个webapi的返回类型
public class ApiResult { /// <summary> /// 说明信息 /// </summary> public string Message { get; set; } /// <summary> /// 返回状态码 /// </summary> public int StatusCode { get; set; } /// <summary> /// 是否成功 /// </summary> public bool IsSuccess { get; set; } }
因为数据的校验很多种类:非空校验、字符串长度校验、电话号码校验、身份证号校验等等,这里我们给这些校验的特性定义一个父类AbstractValidateAttribute:Attribute
,暴露一个校验的虚方法,让这些子类特性去继承重写
public abstract class AbstractValidateAttribute:Attribute { public abstract ApiResult Validate(Object obj); }
非空校验RequiredAttribute
public class RequiredAttribute:AbstractValidateAttribute { public string _ErrorMessage; public override ApiResult Validate(object obj) { if (obj != null && !string.IsNullOrWhiteSpace(obj.ToString()) && obj.ToString() != "0") { return new ApiResult() { IsSuccess = true }; } else { return new ApiResult() { Message = _ErrorMessage, IsSuccess = false }; } } }
长度校验LengthValiDateAttribute
public class LengthValiDateAttribute:AbstractValidateAttribute { //一般设置一个public字段来指定显示对应的信息 public string ErrorMessage; private readonly int _min; private readonly int _max; public LengthValiDateAttribute(int min,int max) { _min = min; _max = max; } public override ApiResult Validate(object obj) { if (obj.ToString().Length < _min || obj.ToString().Length > _max) { return new ApiResult() { IsSuccess = false,
//如果失败则将失败信息赋值给Message Message = ErrorMessage }; } return new ApiResult() { IsSuccess = true }; } }
定义一个工具类,和静态方法去做校验
public class InvokeValiDateManager { public static ApiResult ValiDate<T>(T t) where T : class { Type type = typeof(T); //遍历其属性 foreach (var propertyInfo in type.GetProperties()) { if (propertyInfo.IsDefined(typeof(AbstractValidateAttribute),true)) { //获取属性的值 var value = propertyInfo.GetValue(t); var attributes = propertyInfo.GetCustomAttributes(true); foreach (var attribute in attributes) { AbstractValidateAttribute attributeAttribute = (AbstractValidateAttribute)attribute; ApiResult result = attributeAttribute.Validate(value); if (result.IsSuccess == false) { return result; } } } } return new ApiResult() { IsSuccess = true }; } }
定义一个实体类UserInfo,打上特性
public class UserInfo { public string Name { get; set; } public string Email { get; set; } [Required(_ErrorMessage = "年龄不能为空")] public int Age { get; set; } [Required(_ErrorMessage = "姓名不能为空")] [LengthValiDate(11, 11, ErrorMessage = "手机号必须为11位数")] public string PhoneNumber { get; set; } public UserStateEnum State { get; set; } }
测试:
//2、字段的可行性校验 UserInfo userInfo = new UserInfo() { Name = "", // Age = 16, PhoneNumber = "1323640748822" }; ApiResult result = InvokeValiDateManager.ValiDate<UserInfo>(userInfo); Console.WriteLine($"验证结果:{result.IsSuccess},信息:{result.Message}"); //结果打印: //验证结果:False,信息:手机号必须为11位数