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);