我们将系统性地介绍 C# 中的特性(Attribute)机制,并演示如何自定义一个特性、应用它,并通过反射在运行时读取和使用它。
在 C# 中,特性(Attribute)本质上是一种元数据(metadata),它本身不会自动执行任何逻辑或产生任何行为。它的作用是在编译时将额外的信息"附加"到程序元素上(如类、方法、属性、参数、程序集等),这些信息会被嵌入到程序集的元数据中。真正让特性"发挥作用"的,是运行时通过反射(Reflection)读取这些元数据,并根据其内容执行相应的逻辑。
一、什么是 C# 特性(Attribute)?
特性(Attribute)是 C# 提供的一种声明式编程机制 ,用于向程序元素(如类、方法、属性、字段、参数、程序集等)附加元数据(metadata)。
这些元数据会被编译器写入程序集(.dll 或 .exe)中,在运行时可以通过 反射(Reflection) 读取。
⚠️ 注意:特性本身不会改变代码的行为,它们只是"标签"。真正起作用的是读取这些标签的代码。
二、C# 内置的常见特性示例
C# 和 .NET 提供了许多内置特性:
| 特性 | 用途 |
|---|---|
[Obsolete] |
标记过时的 API,编译时发出警告或错误 |
[DllImport] |
调用非托管 DLL 函数 |
[Serializable] |
标记类可序列化 |
[Conditional] |
条件编译(仅当定义了指定符号时才调用方法) |
ASP.NET Core 中的 [HttpGet], [FromBody] 等 |
路由与模型绑定 |
三、如何自定义一个特性?
步骤 1:继承 System.Attribute
所有自定义特性必须直接或间接继承自 System.Attribute 类。
步骤 2:使用 [AttributeUsage] 指定适用目标(可选但推荐)
控制该特性可以应用到哪些程序元素上。
示例:定义一个用于标记"API 接口描述"的自定义特性
csharp
using System;
// 指定该特性只能用于方法,且不可继承,允许多次使用
[AttributeUsage(
validOn: AttributeTargets.Method,
AllowMultiple = false,
Inherited = true)]
public class ApiDescriptionAttribute : Attribute
{
public string Title { get; }
public string Summary { get; }
// 构造函数(必需通过构造函数传入必要信息)
public ApiDescriptionAttribute(string title)
{
Title = title;
Summary = string.Empty;
}
// 可选属性(可通过命名参数设置)
public string Version { get; set; } = "1.0";
}
✅ 关键点说明:
- 特性的位置参数(Positional Parameters)通过构造函数传入。
- 命名参数(Named Parameters)通过公共属性或字段设置。
[AttributeUsage]是可选的,但强烈建议使用以提高安全性。
四、应用自定义特性
将特性应用到方法上:
csharp
public class UserService
{
[ApiDescription("获取用户信息", Summary = "根据用户ID返回完整用户资料", Version = "2.1")]
public User GetUserById(int id)
{
return new User { Id = id, Name = "Alice" };
}
[ApiDescription("创建新用户")]
public void CreateUser(User user)
{
// 创建逻辑
}
}
🔍 注意:使用时可以省略
Attribute后缀,即写成[ApiDescription(...)]而不是[ApiDescriptionAttribute(...)],这是 C# 的语法糖。
五、在运行时通过反射读取特性
现在我们编写代码,在运行时扫描某个类型的所有方法,提取其上的 ApiDescriptionAttribute 并输出文档信息:
csharp
using System;
using System.Linq;
using System.Reflection;
public class Program
{
public static void Main()
{
Type type = typeof(UserService);
MethodInfo[] methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance);
foreach (var method in methods)
{
var attr = method.GetCustomAttribute<ApiDescriptionAttribute>();
if (attr != null)
{
Console.WriteLine($"方法: {method.Name}");
Console.WriteLine($" 标题: {attr.Title}");
Console.WriteLine($" 摘要: {attr.Summary}");
Console.WriteLine($" 版本: {attr.Version}");
Console.WriteLine();
}
}
}
}
输出结果:
方法: GetUserById
标题: 获取用户信息
摘要: 根据用户ID返回完整用户资料
版本: 2.1
方法: CreateUser
标题: 创建新用户
摘要:
版本: 1.0
六、高级用法补充
1. 支持多个相同特性(AllowMultiple = true)
如果你希望同一个方法可以标记多个相同特性:
csharp
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class TagAttribute : Attribute
{
public string Name { get; }
public TagAttribute(string name) => Name = name;
}
// 使用
[Tag("user")]
[Tag("security")]
public void SecureMethod() { }
读取时用 GetCustomAttributes<T>()(注意是复数):
csharp
var tags = method.GetCustomAttributes<TagAttribute>();
foreach (var tag in tags)
Console.WriteLine(tag.Name);
2. 特性用于编译时处理(如 Source Generators)
从 C# 9 开始,结合 源生成器(Source Generators) ,特性还可以在编译期被分析,用于生成额外代码(如自动实现 INotifyPropertyChanged、DTO 映射等),这比运行时反射更高效。
七、总结
| 项目 | 说明 |
|---|---|
| 本质 | 特性是元数据,不执行逻辑 |
| 生效方式 | 必须通过反射(或源生成器)主动读取 |
| 自定义步骤 | 继承 Attribute + [AttributeUsage] + 构造函数/属性 |
| 应用场景 | 框架扩展(如 MVC、EF)、日志、权限控制、文档生成、AOP 等 |
| 性能注意 | 反射有开销,高频场景建议缓存反射结果 |