C#之反射、元資料詳解
前言
在本節中主要講述自定義特性、反射。自定義特性允許把自定義元資料與程式元素關聯起來。這些元資料是在編譯過程中建立的,並嵌入程式集中。反射是一個普通的術語,它描述了在執行過程中檢查和處理程式元素的功能。例如,反射執行完成以下任務:
- 列舉型別的成員
- 例項化新物件
- 執行物件的成員
- 查詢型別的資訊
- 查詢程式集的資訊
- 檢查應用於某個型別的自定義特性
- 建立和編譯新程式集
這個列表列出了許多功能,本章中主要介紹部分常用的功能。
自定義特性
一、編寫自定義特性
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 { }View Code
接下來我們介紹第二個庫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(); } } } }View Code
這裡我們還需要設定下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}"); } } } }View Code
上面都打上了詳細備註,完整的專案示例已存放在Github上。有興趣的可以Download下來看看。
總結
本篇文章主要介紹了Type和Assembly類,它們是訪問反射所提供的擴充套件功能的主要入口點。反射是.NET中的重要機制,通過反射,可以在執行時獲得程式或程式集中每一個型別(包括類、結構、委託、介面和列舉等)的成員和成員的資訊。
不是井裡沒有水,而是你挖的不夠深。不是成功來得慢,而是你努力的不夠多。
c#基礎知識詳解系列
歡迎大家掃描下方二維碼,和我一起學習更多的C#知識
&n