一、特性直观感受特性与反射息息相关,学习特性前,建议学习了解下这篇博客:C# 反射
每一个特性都会带来对应得功能(其实这是个错觉)。
如下图,两个特性使我们经常在开发中见到的,Obsolete 可以影响到编译器发出警告,Serializable 可以序列化和反序列化影响程序运行
特性使用中括号声明,特性是一个类且必须直接或者间接继承 Attribute。
看了 Obsolete 与 Serializable 特性使用后,F12 查看一下源码,可以看到这两个特性都是一个类并且继承 Attribute
特性添加后,编译会在元素内部产生 IL,但我们没有办法直接使用的,而且在 metadata 里面会有记录。
这里定义了一个 CustomAttribute 特性,且声明到了 Product 类与其成员上,然后进行编译一下
使用反编译工具,在 bin 目录查看程序集(dll or exe),可以看到与我们写得 C# 源代码一样
点开 Product 类,切换到 IL 语言,可一看到其成员,且在 C# 源代码标记过的地方,都编译成了特性的构造函数
四、特性本身无意义
特性本身是没有用没有意义的。这句话,我们从程序运行也可以验证,下面是个一个简单的例子
这里定义了 CustomAttribute ,且在构造函数里面打印了一句话,在实例化对象是会进行提示
public class CustomAttribute: Attribute
{
public CustomAttribute()
{
Console.WriteLine("CustomAttribute 构造函数");
}
}
再定义一个 Product 类,其被 CustomAttribute 声明
[Custom]
public class Product
{
[Custom]
public int Id { get; set; }
[Custom]
public int GetId()
{
return Id;
}
}
这里在 Main 函数,对类进行实例化,对属性赋值,且调用方法
static void Main(string[] args)
{
Product product= new Product() { Id=1};
Console.WriteLine(product.GetId());
Console.ReadKey();
}
启动,可以看到,程序正常运行,且没有打印 Custom 特性相关的提示信息,所以特性本身是没有意义的。
五、特性 API
特性本身是没有用的,程序运行过程中,我们能找到特性,而且也能应用一下。特性可以声明在类、方法、字段、属性、参数、返回值上,程序运行时可以获取到特性声明的信息。
首先定义 CustomAttribute 特性,且包含 Desc 属性和两个构造函数
public class CustomAttribute: Attribute
{
public string Desc { get; set; }
public CustomAttribute()
{
Console.WriteLine("CustomAttribute 构造函数");
}
public CustomAttribute(string desc)
{
Console.WriteLine("CustomAttribute 有参构造函数");
Desc = desc;
}
}
定义 Product 类,在其本身与成员及方法返回值全部声明 CustomAttribute
[Custom("我在类上")]
public class Product
{
[Custom("我在字段上")]
public string name;
[Custom("我在属性上")]
public string Name { get; set; }
[Custom("我在方法上")]
[return:Custom("我在返回值上")]
public string GetName([Custom("我在参数上")] int id)
{
return Name;
}
}
在 Main 函数,写了一些特性实例化与获取信息的一些 API
static void Main(string[] args)
{
Product product= new Product() { };
Type type = typeof(Product);
if (type.IsDefined(typeof(CustomAttribute), true))
{
CustomAttribute customAttribute = (CustomAttribute)type.GetCustomAttribute(typeof(CustomAttribute),true);
Console.WriteLine(customAttribute.Desc);
}
MethodInfo method = type.GetMethod("GetName");
if (method.IsDefined(typeof(CustomAttribute), true))
{
CustomAttribute customAttribute = (CustomAttribute)method.GetCustomAttribute(typeof(CustomAttribute), true);
Console.WriteLine(customAttribute.Desc);
}
ParameterInfo parameter = method.GetParameters()[0];
if (parameter.IsDefined(typeof(CustomAttribute), true))
{
CustomAttribute customAttribute = (CustomAttribute)parameter.GetCustomAttributes(typeof(CustomAttribute), true)[0];
Console.WriteLine(customAttribute.Desc);
}
ParameterInfo returnParameter = method.ReturnParameter;
if (returnParameter.IsDefined(typeof(CustomAttribute), true))
{
CustomAttribute customAttribute = (CustomAttribute)returnParameter.GetCustomAttribute(typeof(CustomAttribute), true);
Console.WriteLine(customAttribute.Desc);
}
PropertyInfo property = type.GetProperty("Name");
if (property.IsDefined(typeof(CustomAttribute), true))
{
CustomAttribute customAttribute = (CustomAttribute)property.GetCustomAttribute(typeof(CustomAttribute), true);
Console.WriteLine(customAttribute.Desc);
}
FieldInfo field = type.GetField("name");
if (field.IsDefined(typeof(CustomAttribute), true))
{
CustomAttribute customAttribute = (CustomAttribute)field.GetCustomAttribute(typeof(CustomAttribute), true);
Console.WriteLine(customAttribute.Desc);
}
Console.ReadKey();
}
启动,可以看到在成员运行时,拿取到声明特性的各种信息
CustomAttribute customAttribute = (CustomAttribute)field.GetCustomAttribute(typeof(CustomAttribute), true); // 会实例化特性
六、特性实际应用
没有破坏封装的前提下,可以额外的加点消息与行为。
说到这可能有的同学就问,上面说了那么多,在我们平时开发时有什么实际用处吗?答案是:有的,接下来使用实例进行讲解
6.1、添加额外消息场景:分销系统,订单状态开发时通常喜欢使用枚举进行定义,但在 UI 展示其对于描述时有哪些做法呢?
首先,订单状态枚举定义,这里进行简化
public enum OrderStatus
{
///
/// 暂存
///
TemporaryStorage = 0,
///
/// 未审批
///
NotApproved = 10,
///
/// 已审批
///
Approved = 20
// 还有其他许多状态,这里不进行举例
}
第一种:手动维护,拼凑(还会有其他类似的写法,这里不进行一一举例)
列表, 这里有的开发时写了一个类似 K-V 的集合,分表对值与描述进行了,平凑返回给 UI ,进行渲染 select 或者 list
public static DictionaryGetOrderStatusList() { Dictionary orderStatus = new Dictionary (); orderStatus.Add(OrderStatus.Approved.GetHashCode(), "暂存"); orderStatus.Add(OrderStatus.Approved.GetHashCode(), "未审批"); orderStatus.Add(OrderStatus.Approved.GetHashCode(), "已审批"); // 未来扩展状态流,都得手动进行添加 return orderStatus; }
表单,面对数据报表,单个状态对于的描述,通常使用一串的 if 进行获取
public static string GetOrderStatusRemark(OrderStatus orderStatus)
{
if (orderStatus == OrderStatus.Approved)
{
return "暂存";
}
else if(orderStatus == OrderStatus.Approved)
{
return "已审批";
}
else if (orderStatus == OrderStatus.NotApproved)
{
return "未审批";
}
// 未来扩展状态流,都得手动进行添加
return orderStatus.ToString();
}
特性方式:提高开发效率,且公用,易于维护,易于编码
首先定义一个 RemarkAttribute 特性,使用 _Remark 承载我们的描述信息
public class RemarkAttribute : Attribute
{
private string _Remark { get; set; }
public RemarkAttribute(string remark)
{
_Remark = remark;
}
public string GetRemark()
{
return _Remark;
}
}
在状态枚举成员打上特性,且输入每个状态的描述信息
public enum OrderStatus
{
///
/// 暂存
///
[Remark("暂存")]
TemporaryStorage = 0,
///
/// 未审批
///
[Remark("未审批")]
NotApproved = 10,
///
/// 已审批
///
[Remark("已审批")]
Approved = 20
// 还有其他许多状态,这里不进行举例
}
创建一个状态枚举的扩张类,写一个 GetRemark 扩张方法,用于取当前状态成员标记特性的描述信息
public static class OrderStatusExtension
{
public static string GetRemark(this OrderStatus val)
{
Type type = val.GetType();
FieldInfo field = type.GetField(val.ToString());
if (field.IsDefined(typeof(RemarkAttribute),true))
{
RemarkAttribute remarkAttribute = (RemarkAttribute)field.GetCustomAttribute(typeof(RemarkAttribute), true);
return remarkAttribute.GetRemark();
}
return val.ToString();
}
}
启动,可以看到,成功从 Remark 特性成员 _Remark 字段获取到 NotApproved 状态的描述信息
static void Main(string[] args)
{
OrderStatus orderStatus = OrderStatus.NotApproved;
Console.WriteLine(orderStatus.GetRemark());
Console.ReadKey();
}
延展:平时开发程序,model 对于的表格或者表单的描述也可用此方法
场景:表单提交,数据效验
public class Personal
{
public string Name { get; set; }
public string Address { get; set; }
public long QQ { get; set; }
// 还有许多其他字段
}
第一种:这种写法最简单的,通常在 mvc 的 action 方法里面进行效验,在多个 action 中这种会造成代码重复,且维护困难
Personal personal= new Personal() { QQ = 1};
if (personal.QQ > 10001 && personal.QQ < 999999999999)
{
}
// 还有更多 else if () 对其他进行效验
第二种:由于第一种产生的问题,有的小伙伴会将验证逻辑放到 get set 访问器中,这种造成了职责不分明,实体本身是承载信息的,不需要存在业务逻辑
public class Personal
{
public string Name { get; set; }
public string Address { get; set; }
public long QQ
{
get
{
return this.QQ;
}
set
{
if (value > 10001 && value < 999999999999)
{
this.QQ = value;
}
else
{
throw new Exception("QQ号不合法");
}
}
}
// 还有许多其他字段
}
特性方式:由于以上两种产生的问题,我们可以使用特性进行处理,易于维护、易于编码、易于公用
定义一个 LongAttribute 特性,其中有个最小、最大字段,且有一个 Validate 方法用于处理效验逻辑
public class LongAttribute : Attribute
{
private long _Min { get; set; }
private long _Max { get; set; }
public LongAttribute(long min,long max)
{
_Min = min;
_Max = max;
}
public bool Validate(object val)
{
if (val != null && !string.IsNullOrEmpty(val.ToString()))
{
if (long.TryParse(val.ToString(),out long lResult))
{
if (lResult > _Min && lResult < _Max)
{
return true;
}
}
}
return false;
}
}
定义一个实体类,有 Name、Address 、QQ 三个字段(实际比这多),在 QQ 字段标记特性 Long 并定义 min max
public class Personal
{
public string Name { get; set; }
public string Address { get; set; }
[Long(1001, 99999999999)]
public long QQ { get; set; }
// 还有许多其他字段
}
定义一个 Validate 的扩张类与扩张方法,其作用是取得对象 Type 获取 LongAttribute 并调用其 Validate 进行业务逻辑效验
public static class ValidateExtension
{
public static bool Validate(this object val)
{
Type type= val.GetType();
foreach (var prop in type.GetProperties())
{
if (prop.IsDefined(typeof(LongAttribute),true))
{
LongAttribute longAttribute = (LongAttribute)prop.GetCustomAttribute(typeof(LongAttribute),true);
if(!longAttribute.Validate(prop.GetValue(val)))
{
return false;
}
}
}
return true;
}
}
这里,分别实例化两个 Personal ,并对 QQ 字段赋予不合法与合法的值
static void Main(string[] args)
{
Personal personal= new Personal() { QQ = 1};
Console.WriteLine(personal.Validate());
Personal personal1= new Personal() { QQ = 100000};
Console.WriteLine(personal1.Validate());
Console.ReadKey();
}
启动,可以看到对效验的结果,分别返回了 false true 成功的进行了效验
有人要问只验证个 QQ 有啥意义?且慢,我们还可以多名字等其他字段进行验证,接着我们对 name 字段进行效验
定义一个 LengthAttribute 特性,定义 min max 两个成员,Validate 方法进行业务逻辑效验
public class LengthAttribute : Attribute
{
private long _Min { get; set; }
private long _Max { get; set; }
public LengthAttribute(long min, long max)
{
_Min = min;
_Max = max;
}
public bool Validate(object val)
{
if (val != null && !string.IsNullOrEmpty(val.ToString()))
{
if (val.ToString().Length > _Min && val.ToString().Length < _Max)
{
return true;
}
}
return false;
}
}
在 name 字段打上 Length 特性,且定义 min = 1,max =10
public class Personal
{
[Length(1, 10)]
public string Name { get; set; }
public string Address { get; set; }
[Long(1001, 99999999999)]
public long QQ { get; set; }
// 还有许多其他字段
}
在 ValidateExtension 类 Validate 方法添加一个 LengthAttribute 特性的检查
public static class ValidateExtension
{
public static bool Validate(this object val)
{
Type type= val.GetType();
foreach (var prop in type.GetProperties())
{
if (prop.IsDefined(typeof(LongAttribute),true))
{
LongAttribute longAttribute = (LongAttribute)prop.GetCustomAttribute(typeof(LongAttribute),true);
if(!longAttribute.Validate(prop.GetValue(val)))
{
return false;
}
}
if (prop.IsDefined(typeof(LengthAttribute), true))
{
LengthAttribute longAttribute = (LengthAttribute)prop.GetCustomAttribute(typeof(LengthAttribute), true);
if(!longAttribute.Validate(prop.GetValue(val)))
{
return false;
}
}
}
return true;
}
}
实例化 Personal ,对 name 字段进行赋值
static void Main(string[] args)
{
Personal personal= new Personal() { Name = "张三"};
Console.WriteLine(personal.Validate());
Personal personal1= new Personal() { Name = "张"};
Console.WriteLine(personal1.Validate());
Console.ReadKey();
}
启动,可以看到,根据标记的特性正确的进行了检测
抽象
public class AbstractValidateAttribute : Attribute
{
public virtual bool Validate(object val) => false;
}
public class LongAttribute : AbstractValidateAttribute
{
private long _Min { get; set; }
private long _Max { get; set; }
public LongAttribute(long min, long max)
{
_Min = min;
_Max = max;
}
public override bool Validate(object val)
{
if (val != null && !string.IsNullOrEmpty(val.ToString()))
{
if (long.TryParse(val.ToString(), out long lResult))
{
if (lResult > _Min && lResult < _Max)
{
return true;
}
}
}
return false;
}
}
public class LengthAttribute : AbstractValidateAttribute
{
private long _Min { get; set; }
private long _Max { get; set; }
public LengthAttribute(long min, long max)
{
_Min = min;
_Max = max;
}
public override bool Validate(object val)
{
if (val != null && !string.IsNullOrEmpty(val.ToString()))
{
if (val.ToString().Length > _Min && val.ToString().Length < _Max)
{
return true;
}
}
return false;
}
}
static void Main(string[] args)
{
Personal personal= new Personal() { Name = "张",QQ = 9999999};
Console.WriteLine(personal.Validate());
Personal personal1 = new Personal() { Name = "张三", QQ = 1 };
Console.WriteLine(personal1.Validate());
Personal personal2 = new Personal() { Name = "张三", QQ = 9999999 };
Console.WriteLine(personal2.Validate());
Console.ReadKey();
}



