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)] // AllowMultiple=true 时允许重复标记
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继承Student,Inherited=true时通过反射也能在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.GetCustomAttribute<RemarkAttribute>();
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 ValiDate<T>(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 Find<T>(int id) where T : BaseModel
{
Type type = typeof(T);
string tableName = type.IsDefined(typeof(TableNameAttribute), false)
? type.GetCustomAttribute<TableNameAttribute>().Name
: type.Name;
var columns = type.GetProperties().Select(p =>
p.IsDefined(typeof(ColumnNameAttribute), false)
? p.GetCustomAttribute<ColumnNameAttribute>().Name
: p.Name);
string sql = $"SELECT {string.Join(",", columns)} FROM {tableName} WHERE Id={id}";
// ... 执行查询并反射赋值
}
泛型缓存优化反射性能: 同一类型的 SQL 模板每次查询都一样,没必要每次都反射。用泛型类的静态构造函数缓存结果,同一类型只反射一次:
public class ConstantSqlString<T>
{
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 = ConstantSqlString<SysUser>.GetFindSql(1);
ConstantSqlString<SysUser> 和 ConstantSqlString<SysCompany> 是两个独立的类,各自有独立的静态字段,互不干扰,线程安全由 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] |
本文来自博客园,作者:龙猫•ᴥ•,转载请注明原文链接:https://www.cnblogs.com/nullcodeworld/p/19738560

浙公网安备 33010602011771号