C#之反射、元數據詳解
- 2019 年 10 月 3 日
- 筆記
前言
在本節中主要講述自定義特性、反射。自定義特性允許把自定義元數據與程式元素關聯起來。這些元數據是在編譯過程中創建的,並嵌入程式集中。反射是一個普通的術語,它描述了在運行過程中檢查和處理程式元素的功能。例如,反射運行完成以下任務:
- 枚舉類型的成員
- 實例化新對象
- 執行對象的成員
- 查找類型的資訊
- 查找程式集的資訊
- 檢查應用於某個類型的自定義特性
- 創建和編譯新程式集
這個列表列出了許多功能,本章中主要介紹部分常用的功能。
自定義特性
一、編寫自定義特性
1. 理解自定義特性
[LastModified("Test","Test")] public class TestNumber { }
這個例子首先會發現LastModified這個特性,首先把字元串Attribute追加到這個名稱後面,形成一個組合LastModifiedAttribute,然後在其搜多路徑的所有名稱空間去搜索這個名稱的類。注意如果本來就以Attribute結尾了,那麼也就不會組合在一起了。編譯器會找到含有改名稱的類,且這個類直接或間接派生自System.Attribute。編譯器很認為這個類包含控制特性用法的資訊。特別是屬性類需要指定:
- 特性可以應用到那些類型的程式元素上(類、結構、屬性和方法等)
- 是否可以多次應用到同一個應用程式元素上
- 在應用到類和介面上時,是否由派生類和介面繼承
- 這個特性有那些必選和可選參數
如果哦編譯器找不到對應的特性類,或者找到了但是使用方式或者資訊不對,編譯器就會產生一個編譯錯誤。
下面我們看看自定義特性其中的各個元素如何定義吧
2. 指定AttributeUsage特性
第一個要注意的就是AttributeUsage特性,它是特性類的標記。AttributeUsage主要用於標識自定義特性可以應用到那些類型的程式元素上。 這些資訊都是由第一個參數提供的,該參數輸入必選參數,其類型是枚舉類型AttributeTargets。其成員如下:
| All | 32767 | 可以對任何應用程式元素應用屬性。 |
| Assembly | 1 | 可以對程式集應用屬性。 |
| Class | 4 | 可以對類應用屬性。 |
| Constructor | 32 | 可以對構造函數應用屬性。 |
| Delegate | 4096 | 可以對委託應用屬性。 |
| Enum | 16 | 可以對枚舉應用屬性。 |
| Event | 512 | 可以對事件應用屬性。 |
| Field | 256 | 可以對欄位應用屬性。 |
| GenericParameter | 16384 | 可以對泛型參數應用屬性。 目前,此屬性僅可應用於 C#、Microsoft 中間語言 (MSIL) 和已發出的程式碼中。 |
| Interface | 1024 | 可以對介面應用屬性。 |
| Method | 64 | 可以對方法應用屬性。 |
| Module | 2 | 可以對模組應用屬性。 Module 引用的是可移植可執行文件(.dll 或 .exe),而不是 Visual Basic 標準模組。 |
| Parameter | 2048 | 可以對參數應用屬性。 |
| Property | 128 | 可以對屬性 (Property) 應用屬性 (Attribute)。 |
| ReturnValue | 8192 | 可以對返回值應用屬性。 |
| Struct | 8 | 可以對結構應用屬性,即值類型。 |
在上面列表中,有兩個值不對應於任何程式元素:Assembly和Module。特性可以應用到整個程式集或模組中,而不是應用到程式碼中的一個元素上,在這種情況下,這個特性可以放在源程式碼的任何地方,但需要關鍵字Assembly和Module作為前綴
[assembly:SupportsWhatsNew] [module: SupportsWhatsNew]
下面我們再介紹幾個參數AllowMultiple表示一個特性是否可以多次應用到同一項,Inherited表示應用到類或介面上的特性是否可以自動應用到所以的派生的類或介面上。如果特性應用到方法或者屬性上,就表示是否可以自動應用到該方法或屬性等的重新版本上。
二、自定義特性示例
經過上面的介紹,下面我們開始定義自定義特性示例。這裡我們將創建兩個類庫,第一個WhatsNewAttributes庫程式集,其中定義了兩個特性,LastModifiedAttribute和SupportsWhatsNewAttribute。
LastModifiedAttribute特性可以用於標記最後一次修改數據項的時間,它有兩個必選參數:修改的日期和包含描述修改的資訊。還有一個可選參數issues,它可以用來描述該數據項的任何重要問題。
SupportsWhatsNewAttribute是一個較小的類,不帶有任何參數的特性。這個特性是一個程式集的特性,用於把程式集標記為通過SupportsWhatsNewAttribute維護的文檔。

/// <summary> /// 用於標記最後一次修改數據項的時間和資訊。 /// </summary> [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Constructor, AllowMultiple = true, Inherited = false)] public class LastModifiedAttribute : Attribute { private readonly DateTime _dateModified; private readonly string _changes; public LastModifiedAttribute(string dateModified, string changes) { _dateModified = DateTime.Parse(dateModified); _changes = changes; } public DateTime DateModified => _dateModified; public string Changes => _changes; public string Issues { get; set; } } /// <summary> /// 用於把程式集標記為通過LastModifiedAttribute維護的文檔 /// </summary> [AttributeUsage(AttributeTargets.Assembly)] public class SupportsWhatsNewAttribute : Attribute { }
接下來我們介紹第二個庫VectorClass。VectorClass庫引用了WhatsNewAttributes庫,添加聲明後我們使用全局程式集特性標記程式集。

[assembly:SupportsWhatsNew] namespace VectorClass { [LastModified("2017-7-19", "更新C#7,.NET Core 2")] [LastModified("2015-6-6", "更新C#6,.NET Core")] [LastModified("2010-2-14", "修改第一步")] public class Vector : IFormattable, IEnumerable<double> { public Vector(double x, double y, double z) { X = x; Y = y; Z = z; } public Vector(Vector vector) : this(vector.X, vector.Y, vector.Z) { } public double X { get; } public double Y { get; } public double Z { get; } public IEnumerator<double> GetEnumerator() { throw new NotImplementedException(); } [LastModified("2017-7-19", "將ijk格式從StringBuilder更改為格式字元串")] public string ToString(string format, IFormatProvider formatProvider) { if (format == null) { return ToString(); } switch (format.ToUpper()) { case "N": return "|| " + Norm().ToString() + " ||"; case "VE": return $"( {X:E}, {Y:E}, {Z:E} )"; case "IJK": return $"{X} i + {Y} j + {Z} k"; default: return ToString(); } } public double Norm() => X * X + Y * Y + Z * Z; IEnumerator IEnumerable.GetEnumerator() { throw new NotImplementedException(); } [LastModified("2015-6-6", "修改")] [LastModified("2010-2-14", " 類創建")] public class VectorEnumerator : IEnumerator<double> { public double Current => throw new NotImplementedException(); object IEnumerator.Current => throw new NotImplementedException(); public void Dispose() { throw new NotImplementedException(); } public bool MoveNext() { throw new NotImplementedException(); } public void Reset() { throw new NotImplementedException(); } } } }
這裡我們還需要設置下csproj項目文件添加
<version>2.1.0</version>
到這裡我們介紹了自定義特性相關。接下來我們介紹反射,然後根據反射示例加上自定義特性示例去完成一個小的demo。
反射
反射是.NET中的重要機制,通過反射,可以在運行時獲得程式或程式集中每一個類型(包括類、結構、委託、介面和枚舉等)的成員和成員的資訊。有了反射,即可對每一個類型了如指掌。另外我還可以直接創建對象,即使這個對象的類型在編譯時還不知道。
一、System.Type類
Type t=typeof(double);
這裡使用Type類只為了存儲類型的引用,以前把Type看做一個類,實際上時一個抽象的基類。實例化一個Type對象,實際上就實例化了Type的一個派生類。儘管一般情況下派生類只提供各種Type方法和屬性的不同重載,但是這些方法和屬性返回對應數據類型的正確數據。通常,獲取指定任何給定類型的Type引用有3中常用的方式:
- 使用typeof運算符,就想上面的例子一樣
- 使用GetType()方法,所有的類都會從System.Object繼承這個方法。
double d = 10; Type t = d.GetType();
- 調用Type類的靜態方法GetType()
Type t = Type.GetType("System.Double");
Type是實現許多反射功能的入口,它實現了許多方法和屬性,這裡我們將介紹如何使用這個類。
| 屬性 | 返回值 |
| Name | 數據類型名稱 |
| FullName | 數據類型的完全限定名(包括名稱空間名) |
| Namespace | 在其中定義數據類型的名稱空間名 |
其次,屬性還可以進一步獲取Type對象的引用,這些引用表示相關的類
| 屬性 | 返回對應的Type引用 |
| BaseType | 該Type的直接基本類型 |
| UnderlyingSystemType | 該Type在.NET運行庫中映射的類型。這個成員只能在完整的框架中使用 |
其中還有許多布爾屬性表示這種類型是否是一個類。還是一個枚舉等等。這些特性包括IsAbstract、IsArray、IsClass、IsEnum、IsInterface、IsPointer、IsPrimitive(一種預定義的基元數據類型)、IsPublic、IsSealed以及IsValueType。例如判斷類型是否是數組:
Type t = typeof(double); if (t.IsArray)//返回布爾值 { }
二、方法
System.Type的大多數方法都用於獲取對應數據類型的成員資訊:構造函數、屬性、方法和事件等。下面我們看看Type的成員方法,這裡遵循一個模式。注意名稱為複數形式的方法返回一個數組。
| 返回的對象類型 | 方法 |
| ConstructorInfo | GetConstructor(),GetConstructors() |
| EventInfo | GetEvent(),GetEvents() |
| FieldInfo | GetField(),GetFields() |
| MemberInfo | GetMember(),GetMembers(),GetDefaultMembers() |
| MethodInfo | GetMethod(),GetMethods() |
| PropertyInfo | GetProperty(),GetProperties() |
GetMember()和GetMembers()方法返回的數據類型的任何成員或所有成員的詳細資訊,不管這些成員是構造函數、屬性、方法等
三、Assembly類
Assembly類在System.Reflection名稱空間定義,它允許訪問給定程式集的元數據,它也可以包含可以載入和執行程式集的方法。
我們可以先看第一個方法Assembly.Load()或者Assembly.LoadFrom()。這兩個方法的區別在於Load方法的參數時程式集的名稱,運行庫會在各個位置搜索該程式集,試圖找到該程式集,這些位置包括本地目錄和群居程式集快取。
1、獲取在程式集好難過定義的類型的詳細資訊
這裡我跟根據Assembly類的一個功能來獲取程式集中定義的所有類型的詳細資訊,只要調用Assembly.GetTypes()方法,他就可以返回一個包含所有類型的詳細資訊的System.Type引用數組。
Assembly theAssembly = Assembly.Load(new AssemblyName("VectorClass")); Type[] types = theAssembly.GetTypes();
2、獲取自定義特性的詳細資訊
用於查找在程式集或類型中定義了什麼自定義特性的方法取決於與該特性相關的對象類型。如果要確定程式集從整體上關聯了什麼自定義特性,就需要調用Assembly類的一個靜態方法
Attribute[] attributes = Attribute.GetCustomAttributes(theAssembly);
完成示例
到這裡我們就簡單的介紹了自定義特性以及反射,我們就接著完成我們的示例,剛剛以及定義了兩個程式集以及自定義特性。現在我們要做的就是配合反射來獲取相關程式集的資訊。主要實現效果是:說明公司如何定期升級軟體,自動記錄升級的資訊。

class Program { /// <summary> /// 輸出的消息 /// </summary> private static readonly StringBuilder outputText = new StringBuilder(1000); /// <summary> /// 存儲的時間 /// </summary> private static DateTime backDateTo = new DateTime(2017,2,1); static void Main(string[] args) { //獲取訪問的程式集 Assembly theAssembly = Assembly.Load(new AssemblyName("VectorClass")); //獲取自定義特性的詳細資訊 Attribute supportsAttribute = theAssembly.GetCustomAttribute(typeof(SupportsWhatsNewAttribute)); AddToOutput($"assembly:{theAssembly.FullName}"); if (supportsAttribute==null) { AddToOutput("這個程式集不支援"); return; } else { AddToOutput("定義的類型是:"); } //獲取程式集中定義的公共類型集合 IEnumerable<Type> types = theAssembly.ExportedTypes; foreach ( Type definedType in types) { DisplayTypeInfo(definedType); } Console.WriteLine(backDateTo); Console.WriteLine(outputText.ToString()); Console.ReadLine(); } public static void DisplayTypeInfo(Type type) { if (!type.GetTypeInfo().IsClass) { return; } AddToOutput($"{Environment.NewLine}類 {type.Name}"); //獲取類型的詳細資訊然後獲取其自定義詳細資訊選擇自定義特性再篩選時間 IEnumerable<LastModifiedAttribute> lastModifiedAttributes = type.GetTypeInfo().GetCustomAttributes() .OfType<LastModifiedAttribute>().Where(a => a.DateModified >= backDateTo).ToArray(); if (lastModifiedAttributes.Count()==0) { AddToOutput($"t這個{type.Name}沒有改變{Environment.NewLine}"); } else { foreach (LastModifiedAttribute item in lastModifiedAttributes) { WriteAttributeInfo(item); } AddToOutput("這些類的修改方法:"); //獲取類的資訊中的方法 foreach (MethodInfo methond in type.GetTypeInfo().DeclaredMembers.OfType<MethodInfo>()) { //獲取這些方法的自定義特性資訊篩選時間 IEnumerable<LastModifiedAttribute> attributesToMethods = methond.GetCustomAttributes().OfType<LastModifiedAttribute>() .Where(a => a.DateModified >= backDateTo).ToArray(); if (attributesToMethods.Count()>0) { AddToOutput($"{methond.ReturnType}{methond.Name}()"); foreach (Attribute attribute in attributesToMethods) { WriteAttributeInfo(attribute); } } } } } static void AddToOutput(string Text) => outputText.Append("n" + Text); private static void WriteAttributeInfo(Attribute attribute) { if (attribute is LastModifiedAttribute lastModifiedAttribute) { AddToOutput($"tmodified:{lastModifiedAttribute.DateModified:D}:{lastModifiedAttribute.Changes}"); if (lastModifiedAttribute.Issues!=null) { AddToOutput($"tOutstanding issues:{lastModifiedAttribute.Issues}"); } } } }
上面都打上了詳細備註,完整的項目示例已存放在Github上。有興趣的可以Download下來看看。
總結
本篇文章主要介紹了Type和Assembly類,它們是訪問反射所提供的擴展功能的主要入口點。反射是.NET中的重要機制,通過反射,可以在運行時獲得程式或程式集中每一個類型(包括類、結構、委託、介面和枚舉等)的成員和成員的資訊。
不是井裡沒有水,而是你挖的不夠深。不是成功來得慢,而是你努力的不夠多。
歡迎大家掃描下方二維碼,和我一起學習更多的C#知識
