在 C# 开发中,“特性(Attribute)”是一个高频出现、却极易与“属性(Property)”混淆的概念。
特性并不参与业务逻辑的直接执行,而是作为元数据扩展机制,为代码元素附加“说明信息”,并在运行时通过反射读取,是 ORM、序列化、验证框架、AOP 等体系的核心基础。
本文将从概念本质、内置特性、自定义特性、反射读取、实战应用与设计思想六个层面,系统、深入地解析 C# 特性。
一、特性的核心概念:元数据的“声明式说明”
1. 什么是特性(Attribute)
特性本质上是一个继承自System.Attribute的特殊类。
它的作用不是“执行代码”,而是:
在编译期写入程序集元数据,在运行期通过反射读取的声明式信息。
关键特征:
- ✔ 编译期生成,存储在程序集元数据中
- ✔ 运行期只读,无法被修改
- ❌ 不会自动生效、不包含业务逻辑
- ✔ 必须配合反射或框架解析才有意义
Attribute = 描述规则
Reflection / Framework = 执行规则
2. 特性(Attribute) vs 属性(Property)
这是初学者最容易混淆的地方,两者完全不是一个维度的概念。
| 对比维度 | 特性(Attribute) | 属性(Property) |
|---|---|---|
| 本质 | 继承自Attribute的类 | 类的成员 |
| 作用 | 为代码元素附加元数据 | 封装字段、控制访问 |
| 存储位置 | 程序集元数据 | 对象实例 / 静态内存 |
| 使用方式 | [Attribute]标注 | obj.Property |
| 是否参与逻辑 | ❌ 否 | ✔ 是 |
| 应用场景 | ORM、验证、序列化、AOP | 封装状态、校验数据 |
一句话总结:
Property 是程序运行的一部分
Attribute 是程序“描述信息”的一部分
3. 特性的使用形式
[Obsolete("该类已过时,请使用 NewClass")] public class OldClass { }说明:
- 特性类命名通常以
Attribute结尾 - 使用时可以省略
Attribute后缀(语法糖) [Custom]等价于[CustomAttribute]
二、内置特性:.NET 提供的元数据能力
1. Obsolete:标记过时 API(最常用)
[Obsolete("该方法已废弃,请使用 NewMethod", false)] public void OldMethod() { }构造参数说明:
[Obsolete(string message, bool error)]- message:编译器提示信息
- error:
false:编译警告(默认)
true:编译错误(禁止使用)
2. 常见内置特性一览
| 特性 | 作用 |
|---|---|
Serializable | 标记类型可被序列化 |
Required | 数据验证必填项 |
DisplayName | UI 显示名称 |
DllImport | P/Invoke 调用非托管代码 |
Conditional | 条件编译方法 |
三、自定义特性:打造业务专属的元数据
1. 自定义特性的三大规则
1. 必须继承System.Attribute
2. 建议使用AttributeUsage约束适用范围
3. 通过构造函数 / 属性定义元数据参数
2. 自定义特性的完整实现
using System; // 自定义特性的使用规则配置 // AttributeTargets.Class | AttributeTargets.Method:该特性仅可应用于类或方法 // AllowMultiple = false:不允许在同一个目标上多次应用该特性 // Inherited = true:该特性可被派生类/重写方法继承 [AttributeUsage( AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] /// <summary> /// 自定义描述特性,用于为类或方法添加元数据描述信息(描述、作者、创建时间) /// </summary> /// <remarks> /// 特性说明: /// 1. 必选参数:描述信息(通过构造函数传入) /// 2. 可选参数:作者、创建时间(通过命名参数设置) /// 3. 适用范围:类、方法(不可用于其他目标如字段、属性等) /// 4. 继承性:派生类/重写方法会继承该特性 /// 5. 唯一性:同一目标仅可应用一次该特性 /// </remarks> public class CustomDescriptionAttribute : Attribute { /// <summary> /// 核心描述信息(必填) /// </summary> /// <value>目标对象的详细描述文本</value> public string Description { get; } /// <summary> /// 作者信息(可选,命名参数) /// </summary> /// <value>特性创建者/维护者的名称</value> public string Author { get; set; } /// <summary> /// 特性创建时间(可选,命名参数) /// </summary> /// <value>默认值为特性实例化时的当前系统时间</value> public DateTime CreateTime { get; set; } = DateTime.Now; /// <summary> /// 初始化 <see cref="CustomDescriptionAttribute"/> 类的新实例(必填构造函数) /// </summary> /// <param name="description">目标对象的核心描述信息,不可为空</param> /// <exception cref="ArgumentNullException">当description为null或空字符串时抛出</exception> public CustomDescriptionAttribute(string description) { // 可选:添加参数校验,确保必填参数有效 if (string.IsNullOrWhiteSpace(description)) { throw new ArgumentNullException(nameof(description), "描述信息不能为空"); } Description = description; } }3. AttributeUsage 参数详解
| 参数 | 说明 |
|---|---|
ValidOn | 可标注目标(Class / Method / Property 等) |
AllowMultiple | 是否允许重复标注 |
Inherited | 是否可被派生类继承 |
| ⚠ 注意: |
Inherited对 方法 仅在virtual / override情况下生效- 对接口实现并不总是有效
4. 特性参数的底层规则(高频考点)
[MyAttr("必填", Level = 1)]- 构造函数参数:必须是编译期常量
- 命名参数:必须是
public set属性 - 支持类型:
基元类型、string、enumType
以上类型的数组
❌ 不允许:
[MyAttr(DateTime.Now)] // 非常量四、反射:运行时读取特性元数据
1. 反射读取特性的标准流程
- 获取
Type / MemberInfo - 调用 `GetCustomAttribute()``
- 解析特性实例中的元数据
2. 完整实战示例(推荐写法)
using System; using System.Reflection; // 自定义描述特性(标记类/方法的描述、作者等信息) [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public class CustomDescriptionAttribute : Attribute { // 描述内容 public string Description { get; set; } // 作者 public string Author { get; set; } // 创建时间 public DateTime CreateTime { get; set; } // 构造函数:初始化描述 public CustomDescriptionAttribute(string description) { Description = description; } } // 标记类过时(指定替代类) [Obsolete("MyClass 已过时,请使用 MyClassV2")] // 给类添加自定义描述特性 [CustomDescription("示例业务类", Author = "开发者A")] public class MyClass { // 标记方法过时(指定替代方法) [Obsolete("请使用 NewMethod")] public void OldMethod() { } // 给方法添加自定义描述特性 [CustomDescription("新业务方法", Author = "开发者B", CreateTime = new DateTime(2025, 1, 1))] public void NewMethod() { } } class Program { static void Main() { // 获取MyClass类型信息 Type type = typeof(MyClass); // 反射获取类的自定义描述特性 var classAttr = type.GetCustomAttribute<CustomDescriptionAttribute>(); // 输出类描述 Console.WriteLine($"类描述:{classAttr?.Description}"); // 获取NewMethod方法信息 MethodInfo method = type.GetMethod("NewMethod"); // 反射获取方法的自定义描述特性 methodAttr = method.GetCustomAttribute<CustomDescriptionAttribute>(); // 输出方法描述 Console.WriteLine($"方法描述:{methodAttr?.Description}"); } }五、AllowMultiple:多特性高级用法(权限 / AOP 基础)
using System; /// <summary> /// 权限标记特性 /// 用于标注方法所需的权限编码,支持为单个方法标记多个权限 /// </summary> [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] // 特性仅作用于方法,允许同一方法多次标注 public class PermissionAttribute : Attribute { /// <summary> /// 权限编码(如"Order.Read"表示订单读取权限) /// </summary> public string Code { get; } /// <summary> /// 初始化权限特性 /// </summary> /// <param name="code">权限编码字符串</param> public PermissionAttribute(string code) => Code = code; } /// <summary> /// 订单业务服务类 /// </summary> public class OrderService { /// <summary> /// 订单导出方法 /// 需要同时具备订单读取(Order.Read)和订单导出(Order.Export)权限 /// </summary> [Permission("Order.Read")] // 标记该方法需要订单读取权限 [Permission("Order.Export")] // 标记该方法需要订单导出权限 public void Export() { } }读取:
// 获取方法上所有权限特性 var permissions = methodInfo.GetCustomAttributes<PermissionAttribute>(); // 遍历输出各权限编码 foreach (var p in permissions) { Console.WriteLine(p.Code); }👉 这是权限系统、拦截器、切面编程的基础形态
六、典型应用场景
1. 标记过时 API
Obsolete- 引导开发者升级接口
2. 数据验证
[Required]、[MaxLength]- ASP.NET Core 模型验证机制
3. ORM 映射
// 用户实体(映射User表) [Table("User")] public class User { // 用户ID(映射user_id列) [Column("user_id")] public int Id { get; set; } }4. 序列化控制
[Serializable][JsonIgnore]
5. AOP / 框架设计
[Log][Transaction][Authorize]
七、性能与使用注意事项
- ⚠ 反射有性能成本,
高频场景应缓存结果 - ⚠ Attribute 只读,运行期不可修改
- ⚠ 必须继承 Attribute 才有效
- ⚠ 不要把业务逻辑写进 Attribute
缓存示例:
static readonly Dictionary<MemberInfo, Attribute> _cache = new();八、总结:Attribute 的设计思想
Attribute 并不“做事情”,
它只负责“描述事情应该如何做”。
它是 C# 中声明式编程的核心工具:
- 解耦规则与实现
- 降低业务代码侵入性
- 支撑框架级能力
当你真正理解 Attribute 时,
你已经从“写功能代码”,迈入了: