目录
一,特性是什么?
特性(Attribute)是用于在运行时传递程序中各种元素(比如类、方法、结构、枚举、组件等)的行为信息的声明性标签。可以通过使用特性向程序添加声明性信息。一个声明性标签是通过放置在它所应用的元素前面的方括号([ ])来描述的。
1 特性定义:特性其实是一个类,是直接/间接继承自Attribute,约定俗成是以Attribut结尾,在标记的时候以[ ]包裹,尾部的Attribut可以省略掉;
2 特性的作用:可以增加额外信息,增加额外功能,但让特性生效需要使用反射;
以上都是较为官方的解释,从本人理解来看:特性的作用就是对程序中所标记元素的另加描述和限限制。就像生活中的坐火车的车票一样,对该行程进行了进一步描述(如哪个站,目的地,出发时间等),同时也对描述信息进行了限制(如你错过发车时间就不能上车),出现任何差错都会视为异常。
二,如何使用特性?
2.1 .Net 框架预定义特性
- Conditional:起条件编译的作用,只有满足条件,才允许编译器对它的代码进行编译。一般在程序调试的时候使用
- DllImport: 用来标记费.net的函数,表明该方法在一个外部的DLL中定义。
- Obsolete: 这个属性用来标记当前的方法已经废弃,不再使用。
- Serializable:用来标记该对象可以被序列化。
-
AttributeUsage :描述了如何使用一个自定义特性类。它规定了特性可应用到的项目的类型:
/// <summary>
/// 自定义特性
/// AllowMultiple =true:标记在特性上的特性,其实是对特性的一种约束;
/// Inherited =true:约束当前特性是否可以继承
/// AttributeTargets.All:当前特性可以标记在所有的元素上
/// AttributeUsage:在定义特性的时候,对特性的一种约束
/// </summary>
[AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = true)]
public class CustomAttribute : Attribute
{
public CustomAttribute()
{
}
}
}
2.2 自定义特性
.Net 框架允许创建自定义特性,用于存储声明性的信息,且可在运行时被检索。创建并使用自定义特性包含四个步骤:
- 声明自定义特性
- 构建自定义特性
- 在目标程序元素上应用自定义特性
- 通过反射调用特性
▲自定义一个特性:
- 定义一个类并且这个类派生自Attribute。
- 类的名字以Attribute后缀结构。
- 为了安全性,可以用一个sealed修饰成一个密封类型(非必须的),以防止被其他类所继承(可以参考预定义特性的源码,其都被sealed所修饰)
using System;
namespace MyAttribute
{
public sealed class CustomAttribute : Attribute
{
}
}
▲通过反射调用标记到字段,属性,方法上的特性。
通过反射,从类型,属性,方法都可以获取特性实例,要求先IsDefined检测再获取实例化。
public static void ReflectionArrtibute<T>(T t) where T : Class
{
Type type = t.GetType();//通过参数来获取Type类型
if (type.IsDefined(typeof(CustomAttribute),true)) //反射调用特性,记得一定要先判断,然后再获取
{
foreach (CustomAttribute attribute in type.GetCustomAttributes()) //把所有标记的特性都实例化了
{
Console.WriteLine($"attribute.Id={attribute.Id}");
Console.WriteLine($"attribute.Name={attribute.Name}");
attribute.Do();
}
///循环获取所有的属性上标记的特性,调用特性中的元素
foreach (PropertyInfo prop in type.GetProperties())
{
if (prop.IsDefined(typeof(CustomAttribute), true))
{
foreach (CustomAttribute attribute in prop.GetCustomAttributes()) //把所有标记的特性都实例化了
{
Console.WriteLine($"attribute.Id={attribute.Id}");
Console.WriteLine($"attribute.Name={attribute.Name}");
attribute.Do();
}
}
}
///循环获取所有的字段上标记的特性,调用特性中的元素
foreach (FieldInfo field in type.GetFields())
{
if (field.IsDefined(typeof(CustomAttribute), true))
{
foreach (CustomAttribute attribute in field.GetCustomAttributes()) //把所有标记的特性都实例化了
{
Console.WriteLine($"attribute.Id={attribute.Id}");
Console.WriteLine($"attribute.Name={attribute.Name}");
attribute.Do();
}
}
}
///循环获取所有方法上标记的特性,调用特性中的元素
foreach (MethodInfo method in type.GetMethods())
{
if (method.IsDefined(typeof(CustomAttribute), true))
{
foreach (CustomAttribute attribute in method.GetCustomAttributes()) //把所有标记的特性都实例化了
{
Console.WriteLine($"attribute.Id={attribute.Id}");
Console.WriteLine($"attribute.Name={attribute.Name}");
attribute.Do();
}
}
}
}
三,为什么要使用特性?
某些时候我们在程序处理过程中为了程序更加健全及安全需要校验一些参数的合法性,比如邮件格式校验等类似的需求,以下以邮件合法及公司CompanyId的范围只能在1000~10000范围为例。刚开始我们最直接也最先想到的就是传统的方式写参数校验。如下所示:
if (string.IsNullOrWhiteSpace(user.Email)) //:这里只判断了邮箱为空,省略了邮箱合法性判断
{
Console.WriteLine("Email 参数不符合!");
//:这里不执行保存,提示用户参数不合法
}
if (user.CompanyId < 1000 || user.CompanyId > 10000)
{
Console.WriteLine("CompanyId 参数不符合,CompanyId范围只能是1000~10000");
//:这里不执行保存,提示用户参数不合法
}
问题:假如某一天业务需求发生了变化,新增了一个参数的校验或者CompanyId的范围发生改变,我们就需要修改代码。
解决:使用特性:可以在不破坏类型封装的前提下,为对象增加额外的信息,执行额外的行为。通过特性我们把公共逻辑移出去,只完成私有逻辑,具体操作流程如下:
1、考虑到程序后期可能会涉及到多种参数的校验,每种参数要实现的校验规则各不相同,所以我们定义一个抽象类,抽象类中定义一个抽象方法,继承自Attribute。
public abstract class AbstractValidateAttribute : Attribute
{
public abstract bool Validate(object oValue);
}
2、分别定义两个类用于实现校验邮箱和判断公司ID范围的逻辑,并继承自上面的抽象类。
//邮箱合法
[AttributeUsage(AttributeTargets.Property)]
public class EmailValidateAttribute:AbstractValidateAttribute
{
public override bool Validate(object value)
{
if (value!=null)
{
return true;//实际的判断
}
else
{
return false;
}
}
}
//公司ID在1000~10000范围
[AttributeUsage(AttributeTargets.Property)]
public class IntValidateAttribute:AbstractValidateAttribute
{
private int _Min = 0;
private int _Max = 0;
public IntValidateAttribute(int min, int max)
{
this._Min = min;
this._Max = max;
}
public override bool Validate(object oValue)
{
return oValue != null && int.TryParse(oValue.ToString(), out int num) && num >= this._Min && num <= this._Max;
}
}
3、通过反射调用特性(采用泛型)
public class BaseDAL
{
public static void Save<T>(T t)
{
Type type = t.GetType();
PropertyInfo[] T2 = type.GetProperties();
bool isSafe = true;
{
foreach (var property in type.GetProperties())
{
//特性类的实例化就在反射发生的时候
object[] oAttributeArray = property.GetCustomAttributes(typeof(AbstractValidateAttribute), true);
foreach (var oAttribute in oAttributeArray)
{
//转为基类对象
AbstractValidateAttribute validateAttribute = oAttribute as AbstractValidateAttribute;
isSafe = validateAttribute.Validate(property.GetValue(t));
if (isSafe)
{
Console.WriteLine($"{oAttribute.ToString()}保存到数据库");
}
else
{
Console.WriteLine($"{oAttribute.ToString()}数据不合法");
}
}
}
}
}
}
4、构建业务类UseModel,并标记相关特性特性
public class UseModel
{
//邮箱
[EmailValidate]
public string Email { get; set; }
//企业ID
[IntValidate(1000,10000)]
public int CompanyID { get; set; }
public UseModel(string email,int id)
{
this.Email = email;
this.CompanyID = id;
}
}
5、主程序调用
static void Main(string[] args)
{
UseModel useModel = new UseModel("小米", 120000);
BaseDAL.Save<UseModel>(useModel);
}
使用特性后,如果新增不同参数类型的校验,只需要新增对应的类,继承自抽象基类AbstractValidateAttribute,在新增的类中实现具体的校验逻辑,并且在需要校验的属性上标记对应的特性即可,方便代码扩展。
四,特性的应用
4.1 特性实现枚举展示描述信息
(1)创建枚举描述特性RemarkAttribute
[AttributeUsage(AttributeTargets.Field)]
public class RemarkAttribute:Attribute
{
public string Remark { get; set; }
public RemarkAttribute(string remark)
{
this.Remark = remark;
}
}
(2)创建枚举(有三个状态:正常,冻结,删除),并进行标记
public enum UserState
{
//正常状态
[Remark("正常状态")]
Normal =0,
//冻结状态
[Remark("冻结状态")]
Frozen =1,
//删除状态
[Remark("删除状态")]
Deleted = 2
}
(3)通过反射调用特性
public static class AttributeExtend
{
public static string GetRemark(this Enum value)
{
Type type = value.GetType();
var field = type.GetField(value.ToString());
if (field.IsDefined(typeof(RemarkAttribute), true))
{
RemarkAttribute attribute = (RemarkAttribute)field.GetCustomAttribute(typeof(RemarkAttribute), true);
return attribute.Remark;
}
else
{
return value.ToString();
}
}
}
(4)主程序调用
static void Main(string[] args)
{
UserState userState = UserState.Frozen;
string reamrk = userState.GetRemark();
Console.WriteLine(reamrk);
}
//打印结果:“冻结状态”