1.什么是 Attribute
1.1 定义
Attribute 是一种“声明式元数据(declarative metadata)”机制。
• 附加位置:程序集、模块、类型、字段、属性、方法、方法参数、方法返回值、事件、泛型参数、局部变量、本地函数、Lambda 表达式、甚至表达式树。
• 本质:编译器把特性的实例化信息序列化到元数据表中;运行期可通过反射读取,或供编译器、分析器、Source Generator 消费。
• 语法:用方括号 [...]
写在目标实体前面,可简写、组合、带命名参数。
1.2 与注释/ XML 的区别
• 注释不参与编译,XML 文档只在 IntelliSense 中可见;而 Attribute 是“可编译、可反射”的元数据。
• 因此 Attribute 可驱动“代码生成”、“运行期行为”或“编译期验证”。
2. 内置 Attribute 全景图
2.1 编译器指令型
• [Obsolete]
:产生警告或错误。
• [Conditional("DEBUG")]
:方法调用在 Release 被编译器擦除。
• [CallerMemberName]
/ [CallerFilePath]
/ [CallerLineNumber]
:编译期自动填充值。
• [GeneratedCode]
:告诉工具“这是生成的代码”。
2.2 CLR/JIT/Interop
• [DllImport]
、[StructLayout]
、[MarshalAs]
、[UnmanagedCallersOnly]
、[SuppressGCTransition]
。
2.3 序列化
• [Serializable]
、[NonSerialized]
、[DataContract]
/[DataMember]
、[JsonProperty]
(System.Text.Json) 等。
2.4 安全
• [AllowNull]
、[NotNull]
、[SecurityCritical]
、[SecuritySafeCritical]
。
2.5 反射/动态
• [Dynamic]
、[Nullable]
、[TupleElementNames]
(编译器自动生成)。
2.6 ASP.NET Core / WCF / WinForms / EF / …
• [HttpGet]
、[Authorize]
、[ApiController]
、[Table]
、[Key]
、[Display]
、[Inject]
等。
2.7 代码分析
• [NotNullWhen]
、[DoesNotReturn]
、[RequiresUnreferencedCode]
、[RequiresDynamicCode]
。
2.8 实验性 API
• [Experimental("DIAG_ID")]
.
3. AttributeUsage:如何限制自定义特性
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct,AllowMultiple = false,Inherited = true)]
public sealed class MySpecialAttribute : Attribute
{// ...
}
• AttributeTargets
枚举是位标志,可叠加。
• AllowMultiple
:同一目标是否允许重复贴多个。
• Inherited
:派生类/重写成员是否“继承”该特性。注意:仅对“类、方法、属性、事件、字段”有效;接口、返回值、参数不会被继承。
• Inherited = false
时,派生类若想保留需重新写一次。
4. Attribute 构造函数与命名参数
4.1 定位参数(positional)
只能出现在构造函数实参列表,顺序必须一致。
4.2 命名参数(named)
必须是 public 非 static 字段或属性,且类型只能是:
• 基本类型(含 string)
• Type
• object(必须是以上类型的常量表达式)
• 一维数组(元素类型同上)
不能是泛型、decimal、DateTime、可空值类型、动态、指针、用户定义类型。
示例:
[MyAttr(42, Description = "Answer", Tags = new[] { "a", "b" })]
5. 运行期读取:System.Reflection
5.1 传统 API
var attrs = typeof(Foo).GetCustomAttributes(typeof(MySpecialAttribute), inherit: true);
• GetCustomAttributes
:返回 object[];可指定继承策略。
• IsDefined
:仅判断是否存在,性能更高。
• Attribute.GetCustomAttribute
:返回单个 Attribute,存在多个时抛 AmbiguousMatchException。
5.2 .NET 4.5+ 的泛型版本
IEnumerable<MySpecialAttribute> attrs =typeof(Foo).GetCustomAttributes<MySpecialAttribute>(inherit: true);
5.3 性能陷阱
• 首次访问元数据会触发类型加载,反射本身有开销。
• 多次反射同一特性可用静态字段缓存:
static readonly MySpecialAttribute cache =Attribute.GetCustomAttribute(typeof(Foo), typeof(MySpecialAttribute)) as MySpecialAttribute;
6. 编译期消费:Roslyn Analyzer & Source Generator
• 分析器通过 Compilation.GetSymbolsWithName
、SemanticModel.GetDeclaredSymbol
等 API 读取 Attribute 元数据,发出诊断。
• Source Generator 可扫描带有特定 Attribute 的类,然后生成额外源文件(如注册表、代理、序列化器)。
context.SyntaxProvider.CreateSyntaxProvider(predicate: (node, _) => node is ClassDeclarationSyntax,transform: (ctx, _) => ctx.SemanticModel.GetDeclaredSymbol(ctx.Node)).Where(symbol => symbol.GetAttributes().Any(a => a.AttributeClass.Name == "AutoRegisterAttribute"))
7. 预定义 Attribute 的“隐藏行为”
7.1 [Serializable]
在元数据中设置 TypeAttributes.Serializable
标志,供 BinaryFormatter/SoapFormatter 使用。
7.2 [MethodImpl(MethodImplOptions.AggressiveInlining)]
直接指导 JIT,而非反射;所以反射拿不到它。
7.3 [CallerMemberName]
编译器在调用点把字符串常量写进 IL,运行期无需反射。
7.4 [UnsafeAccessor]
(.NET 8 preview)
通过 JIT 内部钩子绕过可访问性检查。
8. Attribute 与 AOP(面向切面编程)
• PostSharp、AspectInjector、Castle DynamicProxy、Metalama 等框架:
编译期或运行期扫描特性 → 编织 IL/生成代理 → 执行拦截逻辑。
[LogCall] // 自定义 Attribute
public void Foo() { }
运行期代理重写为:
public void Foo()
{Logger.LogEnter();try { original(); }finally { Logger.LogExit(); }
}
9. 条件编译与 Attribute
• [Conditional("DEBUG")]
仅影响调用点,不影响特性本身。
• 若想特性本身仅在 DEBUG 存在,需要:
#if DEBUG
[SomeDebugOnlyAttr]
#endif
public void Foo() { }
10. Attribute 与 Nullable Reference Type
• [AllowNull]
、[DisallowNull]
、[MaybeNull]
、[NotNull]
等配合可空性分析。
• 编译器利用这些特性改进流分析,不会产生运行时 IL。
11. 泛型与 Attribute
11.1 泛型类型/方法
可以贴特性,如 [JsonConverter(typeof(MyConv<>))]
。
但特性类本身不能是泛型(CLI 限制)。
11.2 泛型参数特性
class Foo<[MyConstraint] T> { }
需 AttributeTargets.GenericParameter
,且只能使用一次。
12. 局部变量 & Lambda
C# 8 起可在局部变量、本地函数、Lambda 参数上使用 [NotNull]
、[EnumeratorCancellation]
等。
void M([EnumeratorCancellation] CancellationToken token) { }
13. Attribute 与记录类型
• record/record struct 本质仍是类/结构,常规贴法即可。
• [property: Required]
用于 record 的 init-only 属性。
14. 模块级与程序集级 Attribute
[assembly: AssemblyVersion("1.2.3.4")]
[assembly: InternalsVisibleTo("My.Tests")]
[module: UnverifiableCode]
• 必须放在文件顶层(namespace 之外)。
• module:
前缀表示作用于模块(很少用)。
15. 特性命名约定
• 类名必须以 Attribute 结尾;使用时可以省略。
• 若同时存在 MySpecial 和 MySpecialAttribute,编译器优先匹配后缀。
16. CLS 兼容性
• 公开可见的自定义 Attribute 需满足 CLS:构造函数和公共字段/属性类型必须 CLS 兼容。
• 用 [assembly: CLSCompliant(true)]
强制检查。
17. 自定义 Attribute 的“实例化”过程
编译器遇到
[MyAttr(123)]
生成元数据:
• 指向MyAttrAttribute
的 TypeRef/TypeDef token
• 构造函数的 MethodRef token
• 定位参数 blob(123)运行期
GetCustomAttributes
时:
• CLR 分配MyAttrAttribute
对象(通过无参或匹配构造函数)
• 设置字段/属性
• 返回给用户代码
注意:特性类必须具有 public 构造函数,且定位参数必须与构造函数匹配。
18. Attribute 继承与接口
• Attribute 类本身可继承(如 ValidationAttribute
),但一个 Attribute 实例只能附加到单个目标。
• 接口不能贴 Attribute,但 [AttributeUsage(AttributeTargets.Interface)]
允许特性用于接口声明本身。
19. 性能优化实战
• 避免在热路径频繁反射,可缓存 static readonly Attribute[]
。
• Source Generator 在编译期生成静态表,实现“零反射”。
• 使用 IsDefined
代替 GetCustomAttributes
做布尔判断。
• 在 NativeAOT 中,使用 [DynamicDependency]
或 rd.xml 防止特性被裁剪。
20. 调试技巧
• VS 的“模块”窗口可查看元数据 token。
• ildasm /metadata
查看 CustomAttribute 表。
• dotnet-dump
SOS:!dumpmd
, !dumpil
可验证特性是否写入。
• 使用 System.Reflection.Metadata
轻量级读取元数据无需加载类型。
21. 常见陷阱
AllowMultiple = false
却重复贴 → 编译错误 CS0579。构造函数参数类型与实参不符 → 编译错误 CS0182。
特性类自身未继承
System.Attribute
→ 编译错误 CS0616。继承链忘记设置
Inherited = true
导致派生类缺失。在 NativeAOT/ILLinker 中忘记根特性 → 运行期
MissingMetadataException
。在 partial 类文件中重复贴程序集级 Attribute → 用
extern alias
或#if
避免。
完整自定义 Attribute 模板
[AttributeUsage(AttributeTargets.Class |AttributeTargets.Method |AttributeTargets.Property,AllowMultiple = true,Inherited = true)]
public sealed class RetryAttribute : Attribute
{// 定位参数public RetryAttribute(int maxRetries){MaxRetries = maxRetries;}public int MaxRetries { get; }// 命名参数public int DelayMilliseconds { get; set; } = 1000;public Type[] ExceptionTypes { get; set; } = Array.Empty<Type>();
}
使用:
[Retry(3, DelayMilliseconds = 500, ExceptionTypes = new[] { typeof(TimeoutException) })]
public async Task<HttpResponseMessage> CallApiAsync() { ... }
消费:
var method = typeof(MyService).GetMethod(nameof(MyService.CallApiAsync))!;
var retry = method.GetCustomAttribute<RetryAttribute>()!;
Console.WriteLine(retry.MaxRetries);