特性本身不执行任何逻辑,仅是编译时嵌入程序集的元数据;真正起作用的是运行时通过反射(如getcustomattribute)显式读取并处理它。

直接说结论:特性(Attribute)本身不执行任何逻辑,它只是元数据;真正干活的是你用反射写出来的那几行 GetCustomAttribute 或 GetCustomAttributes 调用——没这一步,特性就是死的注释。
怎么定义一个能被反射读取的自定义特性
必须继承 Attribute,且推荐加 [AttributeUsage] 明确作用目标和规则。漏掉 AttributeUsage 不报错,但后期容易误用到不支持的位置(比如给方法参数打表特性却想在类上读)。
-
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]是最常见组合,表示只允许标在类或属性上,不允许重复标记,且子类可继承 - 构造函数参数尽量用基础类型(
string、int、bool),避免传入复杂对象——编译器不支持序列化非简单类型到元数据 - 不要在特性里写业务逻辑(比如验证、日志),那是反射调用方的事;特性里只存配置性信息
GetCustomAttribute<t>()</t> 和 GetCustomAttributes(typeof(T)) 的区别
前者返回单个实例(null 表示没找到),后者返回数组(空数组表示没找到)。多数场景用泛型版本更安全,因为类型检查在编译期就完成;非泛型版常用于动态类型判断(比如插件系统遍历所有 Attribute 子类)。
- 如果特性允许重复(
AllowMultiple = true),必须用GetCustomAttributes,否则会丢掉第二个及以后的实例 -
GetCustomAttribute<t>()</t>在 .NET Core 2.0+ 默认只查当前成员,不查基类;要继承需显式传true:prop.GetCustomAttribute<validationattribute>(true)</validationattribute> - 性能上,泛型版略快,因为省去类型转换;但差异微乎其微,优先选可读性
反射读特性时最常见的三个坑
不是代码写错,而是元数据没“活”过来——编译器默认可能不把特性打进程序集,或者运行时根本没加载对应程序集。
- 类库项目若用了
InternalsVisibleTo或AssemblyFlags,确保目标程序集能访问到特性定义所在的程序集,否则GetCustomAttribute返回 null - .NET 5+ 默认启用“属性修剪(Trimming)”,发布自包含应用时,未被直接引用的特性类型可能被移除;需在 csproj 中加:
<trimmerrootassembly include="YourAttributeAssembly"></trimmerrootassembly> - 用
Type.GetType("xxx")动态加载类型时,如果特性定义在另一个未加载的程序集中,反射会找不到该特性类型,导致GetCustomAttribute(typeof(YourAttr))返回 null —— 此时应先用Assembly.LoadFrom确保程序集已载入
真正难的从来不是写特性,而是确保反射调用链上每个环节都“看见”了那段元数据:编译器得认它、IL 得包它、运行时得加载它、你的反射代码得找对位置。少一环,GetCustomAttribute 就安静地返回 null,连警告都没有。











