03-C#.Net-特性-学习笔记
一、特性的本质1.1 什么是特性特性是一个类直接或间接继承自Attribute抽象类命名规范类名默认以Attribute结尾(使用时可省略)应用方式用方括号[]包裹标记在类或类成员上编译后存在与注释不同特性在编译后依然存在于程序集中1.2 特性 vs 注释// 注释编译后不存在仅用于描述 /// summary这是一个Student类/summary // 特性编译后存在可通过反射获取 [Serializable] // 标记类可序列化 [Custom(123)] // 自定义特性 public class Student { }二、自定义特性2.1 定义特性类特性类就是普通的类继承Attribute。构造函数参数是标记时的位置参数公开的属性/字段可以在标记时用命名参数赋值// 使用 AttributeUsage 约束特性的使用方式 [AttributeUsage( AttributeTargets.All, // 可标记的目标(类、方法、属性等) AllowMultiple true, // 是否允许重复标记 Inherited true // 是否可被继承 )] public class CustomAttribute : Attribute { public string _Name { get; set; } // 命名参数标记时可赋值 public int _Age; // 命名参数标记时可赋值 public CustomAttribute(int id) { } // 位置参数 public CustomAttribute(string name) { _Name name; } public void Do() { Console.WriteLine(CustomAttribute.Do()); } }2.2 标记特性特性可以标记在几乎所有代码元素上[Custom(123)] [CustomAttributeChild] // 子类特性同样可以用 public class Student { [Custom(123)] [Custom(123)] // AllowMultipletrue 时允许重复标记 public int Id { get; set; } [Custom(陈大宝)] public string Name { get; set; } [Custom(123456, _Age 17)] // 位置参数 命名参数 public string Description; [Custom(山水, _Name 456789)] public void Study() { } [return: Custom(123)] // 标记在返回值上 public string Answer([Custom(456)] string name) // 标记在参数上 { return $This is {name}; } }2.3 AttributeUsage 详解AttributeUsage本身也是一个特性专门用来约束自定义特性的使用方式三个参数AttributeTargets— 指定特性能标记在哪里。常用值Class、Method、Property、Field、Parameter、All可用|组合多个目标。建议自定义特性时明确指定避免误用[AttributeUsage(AttributeTargets.Property)] // 只能标记在属性上 [AttributeUsage(AttributeTargets.Field)] // 只能标记在字段上 [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] // 类或方法AllowMultiple— 同一位置是否允许重复标记默认false。验证场景里一个属性需要同时标[Required]和[Length]就需要设为true。Inherited— 子类是否继承父类上的特性默认true。StudentVip继承StudentInheritedtrue时通过反射也能在StudentVip上获取到Student的特性。注意接口上的特性不会被实现类继承只有类的继承链有效。三、通过反射调用特性特性标记后无法直接调用必须通过反射获取实例才能使用。关键点特性标记 ≠ 执行反射是让特性生效的唯一途径先用IsDefined()判断是否存在再用GetCustomAttributes()获取实例避免无谓的对象创建GetCustomAttributes(true)的true参数表示同时搜索继承链false只获取当前类型自身的特性类、属性、字段、方法、参数上的特性都可以通过对应的反射对象获取public static void Show(Student student) { Type type student.GetType(); // 获取类上的特性 if (type.IsDefined(typeof(CustomAttribute), true)) { foreach (CustomAttribute attr in type.GetCustomAttributes(true)) { Console.WriteLine(attr._Name); attr.Do(); // 通过反射拿到实例后才能调用方法 } } // 获取属性上的特性 foreach (PropertyInfo prop in type.GetProperties()) { if (prop.IsDefined(typeof(CustomAttribute), true)) { foreach (CustomAttribute attr in prop.GetCustomAttributes(true)) attr.Do(); } } // 获取字段上的特性 foreach (FieldInfo field in type.GetFields()) { if (field.IsDefined(typeof(CustomAttribute), true)) { foreach (CustomAttribute attr in field.GetCustomAttributes(true)) attr.Do(); } } // 获取方法参数上的特性 foreach (MethodInfo method in type.GetMethods()) { foreach (ParameterInfo para in method.GetParameters()) { if (para.IsDefined(typeof(CustomAttribute), true)) { foreach (CustomAttribute attr in para.GetCustomAttributes(true)) attr.Do(); } } } }四、特性的两大核心用途用途一获取额外信息(枚举描述)问题数据库存数字(1、2、3)界面要显示文字描述。传统 if-else 的问题// 传统方式 ❌ if (userState UserStateEnum.Normal) Console.WriteLine(正常状态); else if (userState UserStateEnum.Frozen) Console.WriteLine(已冻结); // 问题分支多、修改描述需要改多处解决把描述直接标记在枚举字段上反射扩展方法统一读取。// 1. 定义描述特性只允许标记在字段上 [AttributeUsage(AttributeTargets.Field)] public class RemarkAttribute : Attribute { private string _Description; public RemarkAttribute(string description) _Description description; public string GetRemark() _Description; } // 2. 标记枚举 public enum UserStateEnum { [Remark(正常状态)] Normal 1, [Remark(已冻结)] Frozen 2, [Remark(已删除)] Deleted 3, [Remark(其他)] Other 4 } // 3. 扩展方法封装反射逻辑调用方无需关心反射细节 public static class RemarkAttributeExtension { public static string GetRemark(this Enum enum) { Type type enum.GetType(); FieldInfo field type.GetField(enum.ToString()); if (field.IsDefined(typeof(RemarkAttribute), true)) { RemarkAttribute attr field.GetCustomAttributeRemarkAttribute(); return attr.GetRemark(); } return enum.ToString(); // 没有标记就返回枚举名称本身 } } // 4. 使用 UserStateEnum state UserStateEnum.Frozen; Console.WriteLine(state.GetRemark()); // 输出已冻结 // 在实体中直接暴露描述属性 public class UserInfo { public UserStateEnum State { get; set; } public string UserStateDescription State.GetRemark(); }新增枚举值只加一行[Remark(xxx)]描述改了只改标记处获取逻辑完全不用动。用途二获取额外功能(数据验证)问题保存前需要验证字段传统 if 判断的问题字段多了代码量爆炸验证逻辑散落各处无法复用解决把验证规则标记在属性上反射统一执行。// 1. 抽象基类定义统一接口(开闭原则扩展新规则只需继承不改已有代码) public abstract class AbstractAttribute : Attribute { public abstract ApiResult Validate(object oValue); } public class ApiResult { public bool Success { get; set; } public string ErrorMessage { get; set; } } // 2. 具体验证规则 [AttributeUsage(AttributeTargets.Property)] public class RequiredAttribute : AbstractAttribute { public string _ErrorMessage; public RequiredAttribute(string message) _ErrorMessage message; public override ApiResult Validate(object value) { bool ok value ! null !string.IsNullOrWhiteSpace(value.ToString()); return new ApiResult { Success ok, ErrorMessage ok ? null : _ErrorMessage }; } } [AttributeUsage(AttributeTargets.Property)] public class LengthAttribute : AbstractAttribute { public string _ErrorMessage; private int _Min, _Max; public LengthAttribute(int min, int max) { _Min min; _Max max; } public override ApiResult Validate(object value) { if (value null) return new ApiResult { Success true }; int len value.ToString().Length; bool ok len _Min len _Max; return new ApiResult { Success ok, ErrorMessage ok ? null : _ErrorMessage }; } } // 3. 标记实体属性 public class UserInfo { public int Id { get; set; } [Required(Name的值不能为空)] public string Name { get; set; } [Required(Mobile的值不能为空)] [Length(11, 11, _ErrorMessage 手机号必须为11位数)] public string Mobile { get; set; } } // 4. 统一验证管理器只依赖抽象基类 public static class ValidateInvokeManager { public static ApiResult ValiDateT(this T t) where T : class { Type type t.GetType(); foreach (PropertyInfo prop in type.GetProperties()) { if (prop.IsDefined(typeof(AbstractAttribute), true)) { object value prop.GetValue(t); // 一个属性可能有多个验证特性逐一执行 foreach (AbstractAttribute attr in prop.GetCustomAttributes()) { ApiResult result attr.Validate(value); if (!result.Success) return result; // 遇到第一个失败立即返回 } } } return new ApiResult { Success true }; } } // 5. 使用 UserInfo user new UserInfo { Id 1, Name 张三, Mobile 123 }; ApiResult result ValidateInvokeManager.ValiDate(user); if (!result.Success) Console.WriteLine(result.ErrorMessage); // 手机号必须为11位数要新增验证规则(如正则、数字范围)只需新建一个继承AbstractAttribute的类ValidateInvokeManager完全不用改。五、特性在自定义 ORM 中的应用问题数据库表名/字段名与类名/属性名不一致ORM 无法自动映射。解决用特性标记映射关系ORM 通过反射读取特性生成正确的 SQL。// 定义映射特性 [AttributeUsage(AttributeTargets.Class)] public class TableNameAttribute : Attribute { public string Name { get; } public TableNameAttribute(string name) Name name; } [AttributeUsage(AttributeTargets.Property)] public class ColumnNameAttribute : Attribute { public string Name { get; } public ColumnNameAttribute(string name) Name name; } // 标记实体 [TableName(LM_User)] public class SysUser : BaseModel { [ColumnName(user_name)] public string Name { get; set; } [ColumnName(user_phone)] public string Phone { get; set; } } // ORM 查询时通过反射读取特性生成 SQL public T FindT(int id) where T : BaseModel { Type type typeof(T); string tableName type.IsDefined(typeof(TableNameAttribute), false) ? type.GetCustomAttributeTableNameAttribute().Name : type.Name; var columns type.GetProperties().Select(p p.IsDefined(typeof(ColumnNameAttribute), false) ? p.GetCustomAttributeColumnNameAttribute().Name : p.Name); string sql $SELECT {string.Join(,, columns)} FROM {tableName} WHERE Id{id}; // ... 执行查询并反射赋值 }泛型缓存优化反射性能同一类型的 SQL 模板每次查询都一样没必要每次都反射。用泛型类的静态构造函数缓存结果同一类型只反射一次public class ConstantSqlStringT { private static readonly string FindSql; static ConstantSqlString() { // 静态构造函数只执行一次之后直接读缓存 Type type typeof(T); FindSql $SELECT {string.Join(,, type.GetProperties().Select(p $[{p.Name}]))} FROM {type.Name} WHERE Id; } public static string GetFindSql(int id) ${FindSql}{id}; } // 使用 string sql ConstantSqlStringSysUser.GetFindSql(1);ConstantSqlStringSysUser和ConstantSqlStringSysCompany是两个独立的类各自有独立的静态字段互不干扰线程安全由 CLR 的类型初始化机制保证无需加锁。六、核心总结特性的三大核心步骤定义特性继承Attribute用AttributeUsage约束使用范围标记特性用[]标记在目标上传位置参数或命名参数反射调用运行时通过反射获取特性实例执行其中的逻辑特性的两大价值获取额外信息枚举描述、配置信息、映射关系获取额外功能数据验证、AOP 切面、权限控制最佳实践明确指定AttributeTargets告诉使用者这个特性用在哪里先用IsDefined()判断再获取避免无谓的对象创建用扩展方法封装反射调用逻辑对外暴露干净的 API用抽象基类统一处理同类特性保持可扩展性反射有性能开销高频调用场景用泛型缓存优化常见框架中的特性速查场景常见特性序列化[Serializable]ASP.NET Core MVC[HttpGet][Route][Authorize][FromBody]依赖注入[Inject][Service]ORM(EF Core)[Table][Column][Key][ForeignKey]数据验证[Required][Range][StringLength][EmailAddress]