Attribute是编译后保留在元数据中的类型化标记,运行时通过反射按需构造实例;它不是注释或宏,不自动执行,未读取则无开销。

Attribute 是什么,不是什么
它不是注释,也不是编译期的宏;它是编译后仍保留在元数据中的类型化标记,运行时可通过反射读取。C# 编译器会把 Attribute 实例序列化进程序集的元数据表(如 CustomAttribute 表),CLR 在加载类型时并不执行它——只有你显式调用 GetCustomAttributes() 等反射方法时,才按需构造实例。
这意味着:没被读取的 Attribute 不会触发构造函数,也不会产生运行时开销;但一旦读取,每次调用都会 new 一个新实例(除非你手动缓存)。
定义自定义 Attribute 的关键约束
必须继承 Attribute 类,且推荐显式声明 AttributeUsage —— 否则默认允许打在所有目标上(类、方法、参数…),容易误用。
-
AttributeUsage的AllowMultiple = false是默认值,若允许多个同类型标签,必须设为true,否则第二次打标会编译报错:error CS0579: Duplicate 'XXXAttribute' attribute - 构造函数参数只能是编译期常量类型(
string、int、typeof(…)、enum、一维数组等),不能传Func<T>或对象实例 - 属性(Property)必须有 public set 访问器才能被赋值;只读属性(只有 get)无法在打标时初始化
示例:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, AllowMultiple = false)]
public class ApiFieldAttribute : Attribute
{
public string Name { get; set; }
public bool Required { get; set; }
public ApiFieldAttribute(string name) // 构造函数参数必须是常量表达式
{
Name = name;
}
}在类和属性上打标签的写法差异
语法一致,但语义和反射读取方式不同:打在类上,属于该 Type 的元数据;打在属性上,属于该 PropertyInfo 的元数据。
- 类上打标:
[ApiField("user_id")] public class User { ... }→ 用typeof(User).GetCustomAttributes(typeof(ApiFieldAttribute), false)读 - 属性上打标:
[ApiField("id", Required = true)] public string Id { get; set; }→ 用typeof(User).GetProperty("Id").GetCustomAttributes(typeof(ApiFieldAttribute), false)读 - 注意
false表示不从基类继承;若父类有同名属性并打了标签,且你想继承,得传true,但多数场景应避免隐式继承标签
反射读取时最容易踩的三个坑
反射本身不慢,但反复调用 GetCustomAttributes() 是性能黑洞;更隐蔽的是类型加载与泛型擦除问题。
- 别在循环里反复调用
GetCustomAttributes<T>():每次调用都触发内部数组分配和类型检查。应提前缓存结果,例如用ConcurrentDictionary<MemberInfo, T[]> -
typeof(List<int>).GetCustomAttributes()可能读不到你预期的标签——因为泛型类型构造后(List<int>)和开放泛型(List<>)是两个 Type,标签只存在于你实际打标的那个 Type 上 - 使用
IsDefined(typeof(T), false)比GetCustomAttributes()更轻量,适合做存在性判断;但不要用它代替读取,因为它不返回实例
真实项目中,绝大多数 Attribute 读取发生在初始化阶段(如 DI 容器扫描、API 文档生成),而非高频请求路径——这点常被忽略,导致过早优化或误判瓶颈。


















