<p>C# Attribute是编译期写入IL元数据、运行时可反射读取的声明式元数据容器,不自动执行逻辑;其行为依赖编译器硬编码识别(如[Obsolete])或框架主动消费(如反射/AOP),非“语法糖”或注解。</p>

Attribute 不是注解,也不是“语法糖”,它是 .NET 运行时可识别的元数据容器——编译后会真实写入程序集的 IL 元数据表中,反射能查到,AOP 框架能拦截,序列化器能读取,编译器能据此报错或警告。搞混这点,后续所有自定义和使用都会踩坑。
为什么 [Obsolete] 能让编译器报错,而注释不能
因为 [Obsolete] 是一个继承自 Attribute 的类,它被编译器硬编码识别。当编译器遇到这个特性时,会检查其构造参数(比如第二个 bool 参数),若为 true,就直接中断编译并抛出 CS0618 错误。
-
[Obsolete]本身不执行任何逻辑,它的作用完全由编译器在编译阶段主动“消费” - 你写
[MyCustom],哪怕它继承了Attribute,编译器也完全无视——除非你自己用反射+Assembly.Load去扫描并处理 - 所有预定义特性(如
[Serializable]、[DllImport]、[Conditional])都依赖运行时或编译器的“约定式支持”,不是自动生效的
AttributeUsage 决定特性能不能贴、贴在哪、贴几次
这是自定义特性的第一道闸门。漏设或设错,轻则编译报错 CS0592(特性不可用于此声明类型),重则特性被静默忽略。
- 必须显式加
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]才能用在类和方法上 -
AllowMultiple = true才允许同一目标贴多个同名特性,比如[Conditional("DEBUG"), Conditional("TEST")] -
Inherited = false表示子类不会自动继承父类上的该特性(对基类方法重写时尤其关键) - 别用
AttributeTargets.All图省事——它会让特性意外出现在字段、参数甚至程序集上,后期排查元数据污染极难
构造函数参数 vs 命名参数:哪些能传类型,哪些不能
特性构造函数只接受编译期常量:string、int、typeof(...)、enum、null,以及它们的一维数组。命名参数(property)同理,但更受限。
- 可以:
[MyAttr("log", Level = LogLevel.Warning)](Level是enum) - 可以:
[MyAttr(typeof(List<int>))]</int>(typeof是编译期常量) - 不可以:
[MyAttr(new List<int>())]</int>(运行时对象,编译失败) - 不可以:
[MyAttr(info = typeof(GenericClass<int t string>))]</int>(泛型类型参数含未绑定变量T,报CS0416) - 泛型类型引用必须是开放式(
GenericClass)或封闭式(GenericClass<int string></int>),不能是部分构造
反射读取特性的三步避坑法
你以为 GetCustomAttribute<t>()</t> 就完事了?实际中 70% 的问题出在没控制好查找范围和继承链。
- 用
GetCustomAttribute<t>(inherit: false)</t>显式关掉继承,避免子类意外拿到父类的特性 - 方法参数上的特性要调用
parameter.GetCustomAttribute<t>()</t>,不是从方法本身读——参数和方法是独立元数据节点 - 全局特性(如
[assembly: AssemblyVersion])必须从Assembly.GetExecutingAssembly()或加载后的程序集读,不能用typeof(X).Assembly去猜 - 别在热重载(Hot Reload)场景下缓存反射结果——程序集可能被替换,
Type对象失效,GetCustomAttributes()可能返回空


















