1. 程式人生 > >C#反射與特性(九):全網最全-解析反射

C#反射與特性(九):全網最全-解析反射

目錄

  • 1,判斷型別
    • 1.1 類和委託
    • 1.2 值型別
    • 1.3 介面
    • 1.4 陣列
  • 2, 型別成員
    • 2.1 類
    • 2.2 委託
    • 2.3 介面
    • 2.4 可空型別

【微信平臺,此文僅授權《NCC 開源社群》訂閱號釋出】

本篇主要研究型別、型別成員的各種資訊和標識,通過反射的操作將資訊解析出來。

本文主目的的通過反射操作,生成輸出類似下圖的資訊。

在此之前記一下:

C# 中的訪問修飾符:public、private、protected、internal、protected internal。

C# 兩個成員關鍵字 readonly、const。

C# 宣告修飾符: sealed、static、virtual、new 、abstract、override。

我們根據反射的型別物件,大概分為:類、值型別、陣列、結構體、列舉、介面、抽象類、委託、事件、各種泛型(泛型類、泛型方法、泛型建構函式等)。

此文花了我一天半時間,除了寫文章外,檢視大量文件資料,建立了很多專案,進行了大量測試驗證,最終整理出來。

至此此係列已經進行到第九篇啦。

1,判斷型別

從 Type 中解析型別資訊,筆者使用思維導圖整理如圖

判斷是否某種型別

一般來說,如果有兩個 Type 物件,要判斷兩個 Type 所反射的型別,是否為同一種類型,可以使用 ==

            Type A = typeof(ClassA);
            Type B = typeof(ClassB);
            Console.WriteLine(A == B);

1.1 類和委託

1.1.1 判斷是否型別或委託

Type.IsClass 屬性可以判斷一個型別是否為類或者委託。符合條件的會有普通的類(包括泛型)、抽象類(abstract class)、委託(delegate)。

它可以排除值型別和介面。例如簡單值型別、結構體、列舉、介面。

1.1.2 判斷是否泛型

Type.IsGenericType 屬性可以判斷類或委託是否為泛型型別。

Type.IsGenericTypeDefinition 屬性可以判斷 Type 是否是未繫結引數型別的泛型型別。

Type.IsConstructedGenericType 屬性判斷是否可以此 Type 建立泛型例項。

如果是已繫結引數型別的泛型,則可以使用 Activator.CreateInstance() 等方式例項化型別。

實驗過程:

建立三個型別

    public delegate void DeA();
    public delegate void DeB<T>(T t);

    public class ClassC<T>
    {
        public ClassC(T t) { }
    }

列印輸出

           // 普通委託
            Type typeA = typeof(DeA);
            Console.WriteLine("型別名稱:" + typeA.Name);
            Console.WriteLine("是否為類或委託:" + typeA.IsClass);
            Console.WriteLine("是否為泛型:" + typeA.IsGenericType);
            Console.WriteLine("是否已繫結引數型別:" + typeA.IsGenericTypeDefinition);
            Console.WriteLine("可以用此 Type 建立例項:" + typeA.IsConstructedGenericType);


            // 泛型委託,不繫結引數型別
            Type typeB = typeof(DeB<>);
            Console.WriteLine("\n\n型別名稱:" + typeB.Name);
            Console.WriteLine("是否為類或委託:" + typeB.IsClass);
            Console.WriteLine("是否為泛型:" + typeB.IsGenericType);
            Console.WriteLine("是否已繫結引數型別:" + typeB.IsGenericTypeDefinition);
            Console.WriteLine("可以用此 Type 建立例項:" + typeB.IsConstructedGenericType);

            // 泛型委託,繫結引數型別
            Type typeBB = typeof(DeB<int>);
            Console.WriteLine("\n\n型別名稱:" + typeBB.Name);
            Console.WriteLine("是否為類或委託:" + typeBB.IsClass);
            Console.WriteLine("是否為泛型:" + typeBB.IsGenericType);
            Console.WriteLine("是否已繫結引數型別:" + typeBB.IsGenericTypeDefinition);
            Console.WriteLine("可以用此 Type 建立例項:" + typeBB.IsConstructedGenericType);

            // 泛型類,未繫結引數
            Type typeC = typeof(ClassC<>);
            Console.WriteLine("\n\n型別名稱:" + typeC.Name);
            Console.WriteLine("是否為類或委託:" + typeC.IsClass);
            Console.WriteLine("是否為泛型:" + typeC.IsGenericType);
            Console.WriteLine("是否已繫結引數型別:" + typeC.IsGenericTypeDefinition);
            Console.WriteLine("可以用此 Type 建立例項:" + typeC.IsConstructedGenericType);

            // 泛型型別,已繫結引數
            Type typeD = typeof(ClassC<int>);
            Console.WriteLine("\n\n型別名稱:" + typeD.Name);
            Console.WriteLine("是否為類或委託:" + typeD.IsClass);
            Console.WriteLine("是否為泛型:" + typeD.IsGenericType);
            Console.WriteLine("是否已繫結引數型別:" + typeD.IsGenericTypeDefinition);
            Console.WriteLine("可以用此 Type 建立例項:" + typeD.IsConstructedGenericType);

1.1.3 泛型的引數名稱和泛型限定

獲取泛型型別定義時,泛型引數的名稱

    public class MyClass<T1,T2,T3,T4,T5> { }
            Type type = typeof(MyClass<,,,,>);

            var types = ((System.Reflection.TypeInfo)type).GenericTypeParameters;
            foreach (var item in types)
            {
                Console.WriteLine(item.Name);
            }

輸出

T1
T2
T3
T4
T5

TypeInfo 用於處理各類型別的泛型型別宣告。

《C#反射與特性(四):例項化型別》第三節中,我們探究了泛型的各種例項化方式。

泛型約束

對於類和方法來說,使用泛型版本,可能會進行泛型約束,我們需要將約束解析出來。

Type 中, GetGenericParameterConstraintsGenericParameterAttributes 屬性,可以判斷約束型別。

約束 描述
where T : struct 值型別
where T : class 型別引數必須是引用型別。 此約束還應用於任何類、介面、委託或陣列型別
where T : notnull 型別引數必須是不可為 null 的型別
where T : unmanaged 型別引數必須是不可為 null 的非託管型別,跟struct十分相似,但是unmanaged是不安全的。
where T : new() 型別引數必須具有公共無引數建構函式。 與其他約束一起使用時,new() 約束必須最後指定。 new() 約束不能與 structunmanaged 約束結合使用。
where T : <基類名> 型別引數必須是指定的基類或派生自指定的基類
where T : <介面名稱> 型別引數必須是指定的介面或實現指定的介面。 可指定多個介面約束。 約束介面也可以是泛型。
where T : U 為 T 提供的型別引數必須是為 U 提供的引數或派生自為 U 提供的引數

GetGenericParameterConstraints 可以獲取到引數型別,不過只能對 struct、class、<基類名>、<介面名稱>、T : U 有效。

  • 這些約束之間,有著複雜的衝突關係,這裡就不一一列舉了。
  • 而有些型別本身就代表包含多種約束,例如 struct 本身包含 new()notnull
  • 有些約束條件是可以互相組合的。

從上面看來,要解析泛型約束,不是容易的事。

GenericParameterAttributes 列舉

但是我們來劃分一下,針對不同情況下的組合,來理清一下 Type 和 GenericParameterAttributes 的關係。

先看一下 GenericParameterAttributes 列舉,此列舉是用來描述泛型類或方法上泛型引數約束的。

    public enum GenericParameterAttributes
    {
        None = 0,                       // 無特殊情況
        Covariant = 1,                  // 泛型型別引數是可協變的
        Contravariant = 2,              // 泛型型別引數是逆變的
        VarianceMask = 3,               // Contravariant 和 Covariant 的集合
        ReferenceTypeConstraint = 4,    // 引用型別
        NotNullableValueTypeConstraint = 8, // 是值型別且不為空
        DefaultConstructorConstraint = 16,  // 無引數建構函式
        SpecialConstraintMask = 28          // 所有特殊標記的集合
    }

接下來看看不同約束條件和對應的 GenericParameterAttributes 列舉值。

泛型約束關係

泛型約束有各種衝突關係和約束特性,我們來通過表格和圖片,一一列舉出來。

約束 Type 列舉值 衝突 必須放在開頭
struct 值型別 8,16 只能單獨使用
class 4 struct,notnull,unmanaged,<基類名>
notnull 0 struct,class,unmanaged
unmanaged struct 8,16 只能單獨使用
new() 16 struct,unmanaged 必須放在最後
<基類名> <基類名> 0 struct,notnull,unmanaged
<介面名> <介面名> 0 struct,unmanaged
T : U U 0 struct

注:T : U 使用時雖然不提示與其它約束衝突,如果繼承的約束有衝突,可能會在編譯時或執行期可能會報錯。

<介面名> 編寫程式碼時,不與其它約束衝突,但是實際上有些是不能同時使用的。

unmanaged, BaseInterFace 可以用作約束條件,但是 unmanaged 應該是非託管型別,這裡我們就不考慮了。

泛型約束關係如圖所示:

看完圖片後是不是感覺思路很清晰了呢~

泛型約束比較多,他們有多種組合,但是從上圖,可以判斷組合時:

①(紅)

②(黃)(N個藍)

③(黃)(N個藍)(橙)

④(任意一種顏色)

⑤(N個藍色)

由於程式碼比較多,這裡就不顯示了,程式碼已經上傳至碼雲 解析泛型

關於泛型的反射,可以參考這裡 https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/generics/generics-and-reflection

1.1.4 是否委託

經過前面的操作,已經可以篩選出一個型別是否為型別或委託,那麼判斷一個型別是否為委託,可以使用 IsSubclassOf() ,可以判斷一個 Type 是否為委託型別。

IsSubclassOf() 可以判斷當前 Type 是否派生於 引數中的 Type。

type.IsSubclassOf(typeof(Delegate));

另外,有個多播委託 MulticastDelegate ,可以使用

type.IsSubclassOf(typeof(MulticastDelegate))

1.1.5 訪問修飾符

在名稱空間中說明的類和委託只能使用 public、internal 兩個修飾符修飾訪問許可權。如果不指定的話,預設下是 internal 。

Type 的兩個屬性 IsPublicIsNotPublic可以對此進行識別。

測試:

    public class A { }
    internal class B { }
    class C { }

Main 中輸出

            Type typeA = typeof(A);
            Type typeB = typeof(B);
            Type typeC = typeof(C);

            Console.WriteLine(typeA.Name);
            Console.WriteLine("是否public:    "+typeA.IsPublic);
            Console.WriteLine("是否protected:    " + typeA.IsNotPublic);


            Console.WriteLine("\n"+typeB.Name);
            Console.WriteLine("是否public:    " + typeB.IsPublic);
            Console.WriteLine("是否protected:    " + typeB.IsNotPublic);

            Console.WriteLine("\n" + typeC.Name);
            Console.WriteLine("是否public:    " + typeC.IsPublic);
            Console.WriteLine("是否protected:    " + typeC.IsNotPublic);

輸出結果

A
是否public:    True
是否protected:    False

B
是否public:    False
是否protected:    True

C
是否public:    False
是否protected:    True

1.1.6 密封類、靜態型別、抽象類

密封類是不能被繼承的型別,通過 Type 的 IsSealed 可以判斷。

    public sealed class A { }
            Console.WriteLine(typeof(A).IsSealed);

sealed 也可以修飾委託。

判斷是否為抽象類

    public abstract class MyClass { }
            Console.WriteLine(typeof(MyClass).IsAbstract);

定義類時,static、abstract、sealed 任意兩個不能在一起呼叫。

如果一個類是靜態類,那麼 IsSealedIsAbstract 都是 true。

Type 中沒有判斷類是否為靜態類的屬性或方法,但是可以通過上面的方法判斷是否為靜態類。

我們可以做一下實驗

    public sealed class A { }
    public abstract class B { }
    public static class C { }
            Type typeA = typeof(A);
            Type typeB = typeof(B);
            Type typeC = typeof(C);

            Console.WriteLine("密封類:");
            Console.WriteLine("IsSealed:" + typeA.IsSealed);
            Console.WriteLine("IsAbstract:" + typeA.IsAbstract);

            Console.WriteLine("\n抽象類類:");
            Console.WriteLine("IsSealed:" + typeB.IsSealed);
            Console.WriteLine("IsAbstract:" + typeB.IsAbstract);

            Console.WriteLine("\n靜態類");
            Console.WriteLine("IsSealed:" + typeC.IsSealed);
            Console.WriteLine("IsAbstract:" + typeC.IsAbstract);

輸出結果

密封類:
IsSealed:True
IsAbstract:False

抽象類類:
IsSealed:False
IsAbstract:True

靜態類
IsSealed:True
IsAbstract:True

1.1.7 巢狀類訪問許可權

下面是有關於巢狀型別的 Type 的 屬性。 類和委託都可以使用。

屬性 說明
IsNested 獲取一個指示當前 Type 物件是否表示其定義巢狀在另一個型別的定義之內的型別的值。
IsNestedAssembly 獲取一個值,通過該值指示 Type 是否是巢狀的並且只能在它自己的程式集內可見。
IsNestedFamANDAssem 獲取一個值,通過該值指示 Type 是否是巢狀的並且只對同時屬於自己家族和自己程式集的類可見。
IsNestedFamily 獲取一個值,通過該值指示 Type 是否是巢狀的並且只能在它自己的家族內可見。
IsNestedFamORAssem 獲取一個值,通過該值指示 Type 是否是巢狀的並且只對屬於它自己的家族或屬於它自己的程式集的類可見。
IsNestedPrivate 獲取一個值,通過該值指示 Type 是否是巢狀的並宣告為私有。
IsNestedPublic 獲取一個值,通過該值指示類是否是巢狀的並且宣告為公共的。

1.1.8 特性

索特性的方式有兩種

  • 呼叫 Type 或者 MemberInfo 的 GetCustomAttributes 方法;
  • 呼叫 Attribute.GetCustomAttribute 或者 Attribute.GetCustomAttributes 方法;

《C#反射與特性(七):自定義特性以及應用》中,對特性的使用做了很詳細的介紹,這裡不再贅述。

1.1.9 父類、介面

屬性 說明
BaseType 獲取當前 Type直接從中繼承的型別。
方法 說明
GetInterface(String) 搜尋具有指定名稱的介面。
GetInterfaces() 當在派生類中重寫時,獲取由當前 Type實現或繼承的所有介面。
            Type type = typeof(List<>);
            Console.WriteLine("List<> 的父類為:" + type.BaseType);

            Console.WriteLine("List<> 繼承的介面:");
            Type[] types = type.GetInterfaces();
            foreach (var item in types)
            {
                Console.WriteLine(item.Name);
            }

1.2 值型別

Type.IsValueType 可以判斷一個 Type 是否為值型別,簡單值型別、結構體、列舉,都符合要求。

Type.IsEnum 判斷 Type 是否為列舉。

Type.IsPrimitive 判斷 Type 是否為基礎型別。

通過以下過程可以判斷一個型別屬性何種值型別

        public enum MyTest
        {
            None = 0,   // 不是值型別
            Enum = 1,   // 列舉
            Struct = 2, // 結構體
            Base = 3    // 基礎型別
        }
        public static MyTest Test(Type type)
        {
            if (!type.IsValueType)
                return MyTest.None;

            if (type.IsEnum)
                return MyTest.Enum;

            return type.IsPrimitive ? MyTest.Base : MyTest.Struct;
        }

列舉 Type,有如下方法幫助獲取列舉資訊:

方法 說明
GetElementType() 當在派生類中重寫時,返回當前陣列、指標或引用型別包含的或引用的物件的 Type。
GetEnumName(Object) 返回當前列舉型別中具有指定值的常數的名稱。
GetEnumNames() 返回當前列舉型別中各個成員的名稱。
GetEnumUnderlyingType() 返回當前列舉型別的基礎型別。
GetEnumValues() 返回當前列舉型別中各個常數的值組成的陣列。

1.3 介面

Type.IsInterface 屬性,判斷 Type 是否為介面。

1.4 陣列

IsArray 判斷是否為陣列,GetArrayRank() 獲取陣列的維數。

通過 GetElementType 可以獲取陣列的元素型別

IsSZArray 判斷是否為交錯陣列/鋸齒陣列,IsVariableBoundArray 判斷是否為一維或多維陣列。

IsSZArrayIsVariableBoundArray 是 .NET Core 2.0 以上、.NET Standard 2.1 以上才有的。

            Type a = typeof(int[,,,,]);
            Console.WriteLine(a.Name);
            Console.WriteLine("陣列元素型別:" + a.GetElementType());
            Console.WriteLine("是否為陣列:" + a.IsArray);
            Console.WriteLine("交錯陣列:" + a.IsSZArray);
            Console.WriteLine("一維或多維陣列" + a.IsVariableBoundArray);
            Console.WriteLine("陣列維數:" + a.GetArrayRank());


            Console.WriteLine("\n\n");
            Type b = typeof(int[][][][]);
            Console.WriteLine(b.Name);
            Console.WriteLine("陣列元素型別:" + b.GetElementType());
            Console.WriteLine("是否為陣列:" + b.IsArray);
            Console.WriteLine("交錯陣列:" + b.IsSZArray);
            Console.WriteLine("一維或多維陣列" + b.IsVariableBoundArray);
            Console.WriteLine("陣列維數:" + b.GetArrayRank());

不過 GetElementType() 不能一次性拿到最初的元素型別,GetArrayRank 對交錯陣列也無效。

下面的方法可以快速解析值型別的交錯陣列。

        // 只能解析值型別、系統基礎型別,例如 int 等
        public static (Type, int) Test(Type type)
        {
            if (!type.IsSZArray)
                return (type, 0);

            int num = 0;
            Type that = type;
            while (true)
            {
                that = that.GetElementType();
                num += 1;
                if (that.IsPrimitive)
                    break;
            }
            return (that, num);
        }

呼叫

            Type b = typeof(int[][][][]);
            var result = Test(b);
            Console.WriteLine("元素型別:" + result.Item1);
            Console.WriteLine("鋸齒數:" + result.Item2);

複雜型別的交錯陣列,可以使用字串處理。

2, 型別成員

通過第一章的操作,已經可以解析程式集的大綱圖了,現在開始來獲取型別內部的細節,構建更為清晰的資訊。

解析型別結構,過程大致如下

2.1 類

一個類由以下一個或多個成員組成:

成員型別 說明
PropertyInfo 型別的屬性資訊
FieldInfo 型別的欄位資訊
ConstructorInfo 型別的建構函式資訊
MethodInfo 型別的方法
ParameterInfo 建構函式或方法的引數
EventInfo 型別的事件

特性的話,在《C#反射與特性(七):自定義特性以及應用》已經講解了,這裡不再贅述。

2.1.1 訪問修飾符

public、private兩個修飾符,判斷起來十分簡單;

C# 關鍵字 protectedinternal 在 IL 中沒有任何意義,且不會用於反射 API 中。也就是說在反射中看來,這兩個訪問修飾符沒作用;不過對於獲取資訊來說,還是需要想辦法解析。

protected、internal、protected internal 對於反射呼叫來說,是沒有意義的,不過對於獲取資訊來說,還是需要想辦法解析。

判斷是否為 internal 可以使用 IsAssembly ;判斷是否為 protected internal ,可以使用IsFamilyOrAssembly ;兩個屬性一起用,結果都是 false 的話,則是 protected

屬性 說明
IsAssembly 是否為 internal
IsFamily 是否為 protected
IsFamilyOrAssembly 判斷是否為 protected internal

注:protected internalinternal protected 是一樣的。

下面方法可以判斷並且返回訪問修飾符名稱

        public static string Visibility(FieldInfo field)
        {
            return
                field.IsPublic ? "public" :
                field.IsPrivate ? "private" :
                field.IsAssembly ? "internal" :
                field.IsFamily ? "protected" :
                field.IsFamilyOrAssembly ? "protected internal" :
                null;
        }

2.1.2 其它修飾符

readonly、static、const 三個修飾符,const 不能與其它修飾符同時存在。

屬性 說明
IsLiteral 獲取一個值,通過該值指示該值是否在編譯時寫入並且不能更改
IsStatic static 修飾的欄位,注意 const 也屬於 static。
IsInitOnly 獲取一個值,通過該值指示此欄位是否只能在建構函式的主體中設定

下面的方法可以判斷、返回相應的修飾符

        public static string Only(FieldInfo field)
        {
            if (field.IsLiteral)
                return "const";
            if (field.IsStatic && field.IsInitOnly)
                return "readonly static";
            if (field.IsStatic)
                return "static";
            if (field.IsInitOnly)
                return "readonly";

            return string.Empty;
        }

const int a; 使用 IsStatic 結果為 true,因為 const 也屬於 static。

2.1.3 欄位

通過 2.1.1 和 2.1.2 ,可以解析欄位的資訊了。

下面來測試一下。

定義一個型別

    public class MyClass
    {
        public int a;
        internal int b;
        protected int c;
        protected internal int d;
        private int e;
        public readonly static float f = 1;
    }

輸出解析資料

            Type type = typeof(MyClass);
            FieldInfo[] fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.GetField | BindingFlags.Static | BindingFlags.Instance);
            IEnumerable<FieldInfo> fields1 = type.GetRuntimeFields();
            foreach (var item in fields)
            {
                StringBuilder builder = new StringBuilder();
                builder.Append(GetVisibility(item) + " ");
                builder.Append(GetRead(item) + " ");
                builder.Append(item.FieldType.Name + " ");
                builder.Append(item.Name + " ;");
                Console.WriteLine(builder.ToString());
            }

因為反射的顯示資訊的話,主要是顯示元資料,而且 {get;set;} 屬性會自動生成私有欄位,所以上面的程式碼會將這些也顯示出來。將獲取條件改成 BindingFlags.Public | BindingFlags.GetField | BindingFlags.Static | BindingFlags.Instance

2.1.4 方法、引數

排除屬性的方法

當我們編寫一個屬性,編譯時,編譯器會生成對應的 get 和 set 方法,我們一般來說,只是需要顯示程式設計師編寫的方法,而非系統生成的。

系統生成的屬性的方法,會帶有一個 System.Runtime.CompilerServices.CompilerGeneratedAttribute 特性,通過此特性可以排除系統生成的方法。

        public static bool IsPropertyOfAttr(MethodInfo method)
        {
            return method.GetCustomAttributes().Any(x => x.GetType() == typeof(System.Runtime.CompilerServices.CompilerGeneratedAttribute));
        }
方法的訪問修飾符

判斷方法訪問修飾符的程式碼如下

        public static string GetVisibility(MethodInfo method)
        {
            return
                method.IsPublic ? "public" :
                method.IsPrivate ? "private" :
                method.IsAssembly ? "internal" :
                method.IsFamily ? "protected" :
                method.IsFamilyOrAssembly ? "protected internal" :
                null;
        }

前面已經進行了相應的講解,這裡不在贅述。

重寫與隱藏關鍵字

方法,可以有以下關鍵字修飾:virtual、override、abstract、new;

從繼承關係上來說,分類上,一個方法可能是 virtual、abstract;然後繼承後,重寫 virtual 修飾的方法可能是 override 、 new;abstract 修飾的方法只能使用 override 。

以下屬性可以區分修飾符:

IsAbstractIsVirtualIsHideBySigIsFinal

virtual、override、abstract、new 修飾的方法,IsHideBySig 結果都是 true,可用此屬性判斷方法是否有抽象、重寫等關鍵字修飾。

對於 virtual、override 修飾的方法,IsVirtual 為 true,new 修飾的方法 IsVirtual 為 flase。

但是一個方法,如果是實現了介面方法的話,使用 IsVirtual 也會返回 trueIsHideBySig 也會返回 true。

那麼就剩下區分 virtualoverride 了,如果當前方法是重寫了父類的,使用MethodInfo.GetBaseDefinition() 可以返回當前方法的所重寫父類的方法;如果沒有重寫,那麼就返回方法本身。

IsVirtual 可以判斷當前方法是否可以被重寫。

但是但是,一個在當前類中定義的,類似 public string Test(){} 的方法,可以被重寫,很容易被判斷為 new。需要在最後做個判斷。

當獲取到一個 MethodInfo 時,要區分上面的修飾符,可以使用以下程式碼流程。

        // virtual override  abstract new
        public static string IsOver(Type type,MethodInfo method)
        {
            // 沒有相應的資訊,說明沒有使用以上關鍵字修飾
            if (!method.IsHideBySig)
                return string.Empty;

            // 是否抽象方法
            if (method.IsAbstract)
                return "abstract";

            // virtual、override、實現介面的方法
            if (method.IsVirtual)
            {
                // 實現介面的方法
                if (method.IsFinal)
                    return string.Empty;
                // 沒有被重寫,則為 virtual
                if (method.Equals(method.GetBaseDefinition()))
                    return "virtual";
                else
                    return "override";
            }
            // new
            else
            {
                // 如果是當前型別中定義的方法,則只是一個普通的方法
                if (type == method.DeclaringType)
                    return string.Empty;

                return "new";
            }
        }
獲取返回型別

可以從 ReturnParameterReturnTypeReturnTypeCustomAttributes 獲取有關返回型別的資訊。

ReturnTypeCustomAttributes 是獲取特性資訊的,這裡先不處理。

        // 獲取返回型別
        public static string GetReturn(MethodInfo method)
        {
            Type returnType = method.ReturnType;
            ParameterInfo returnParam = method.ReturnParameter;

            if (returnType == typeof(void))
                return "void";
            if (returnType.IsValueType)
            {
                // 判斷是否 (type1,type2) 這樣的返回
                if (returnParam.ParameterType.IsGenericType)
                {
                    Type[] types = returnParam.ParameterType.GetGenericArguments();
                    string str = "(";
                    for (int i = 0; i < types.Length; i++)
                    {
                        str += types[i].Name;
                        if (i < types.Length - 1)
                            str += ",";
                    }
                    str += ")";
                    return str;
                }
                return returnType.Name;
            }

            // 這裡暫不處理複雜的返回型別,例如陣列,泛型等。
            return returnType.Name;
        }

method.ReturnTypemethod.ReturnParameter.ParameterType 是一樣的。

一般使用 ReturnType 就行了,有些特殊的語法要使用 ReturnParameter

筆者暫時沒有碰到有區分的使用場景。

是否非同步方法

使用以下程式碼判斷是否非同步方法

        public static string GetAsync(MethodInfo info)
        {
            return info.GetCustomAttribute(typeof(AsyncStateMachineAttribute))==null?"":"async ";
        }
泛型方法

通過以下程式碼可以判斷是否為泛型方法,並且返回名稱。

        // 判斷方法是否為泛型方法,並且返回泛型名稱
        public static string GetMethodName(MethodInfo method)
        {
            if (!method.IsGenericMethod)
                return method.Name;
            Type[] types = method.GetGenericArguments();
            string str = method.Name + "<";
            for (int i = 0; i < types.Length; i++)
            {
                str += types[i].Name;
                if (i < types.Length - 1)
                    str += ",";
            }
            str += ">";
            return str;
        }
方法引數

步驟一:判斷引數是否有 in、ref、out 修飾,如果是的話,型別名稱後面會帶有字元 & ;params 的話,會帶有一個 ParamArrayAttribute 特性。

步驟二:獲取引數型別;如果是 in、ref、out 修飾的話,型別名稱後面會帶有一個 &,需要去除;

步驟三:是否具有預設值,如果存在預設值的話,就返回預設值。

        // 解析方法的引數
        public static string GetParams(MethodInfo method)
        {
            ParameterInfo[] parameters = method.GetParameters();
            if (parameters.Length == 0)
                return string.Empty;

            int length = parameters.Length - 1;
            StringBuilder str = new StringBuilder();
            for (int i = 0; i <= length; i++)
            {
                str.Append(InRefOut(parameters[i]) + " ");
                // 這裡不對複雜型別等做處理
                str.Append(GetParamType(parameters[i]) + " ");
                str.Append(parameters[i].Name);
                str.Append(HasValue(parameters[i]) + " ");
                if (i < length)
                    str.Append(",");
            }
            return str.ToString();
        }

        public static string InRefOut(ParameterInfo parameter)
        {
            // in、ref、out ,型別後面會帶有 & 符號
            if (parameter.ParameterType.Name.EndsWith("&"))
            {
                return
                    parameter.IsIn ? "in" :
                    parameter.IsOut ? "out" :
                    "ref";
            }
            if (parameter.GetCustomAttributes().Any(x => x.GetType() == typeof(ParamArrayAttribute)))
                return "params";
            return string.Empty;
        }

        // 獲取型別
        public static string GetParamType(ParameterInfo parameter)
        {
            string typeName = parameter.ParameterType.Name;
            if (typeName.EndsWith("&"))
                typeName = typeName.Substring(0, typeName.Length - 1);
            return typeName;
        }

        // 是否為可選引數,是否有預設值
        public static string HasValue(ParameterInfo parameter)
        {
            if (!parameter.IsOptional)
                return string.Empty;
            object value = parameter.DefaultValue;
            return " = " + value.ToString();
        }

學以致用

學習如何獲取、解析方法的資訊後,我們可以在這裡實踐一下。

定義以下型別,我們最終需要的是 MyClass。

    interface A
    {
        void TestA();
    }
    public abstract class B
    {
        public abstract void TestB();
    }
    public abstract class C : B
    {
        public virtual void TestC()
        {
        }
        public virtual void TestD()
        {
        }
    }
    public class MyClass : C, A
    {
        public void TestA()
        {
            throw new NotImplementedException();
        }

        public override void TestB()
        {
            throw new NotImplementedException();
        }
        public override void TestC()
        {
            base.TestC();
        }
        new public void TestD()
        {

        }
        public (bool, bool) TestE()
        {
            return (true, true);
        }
        public string TestF<T>(T t)
        {
            return t.GetType().Name;
        }
        public string TestG(in string a, ref string aa, out string b, string c = "666")
        {
            b = "666";
            return string.Empty;
        }
        public string TestH(params string[] d)
        {
            return string.Empty;
        }
    }

將 2.1.4 出現的解析方法,複製貼上到專案中,使用以下程式碼即可解析出一個類中的方法。

            Type type = typeof(MyClass);
            MethodInfo[] methods = type.GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly | BindingFlags.Instance);

            foreach (MethodInfo item in methods)
            {
                StringBuilder builder = new StringBuilder();
                builder.Append(GetVisibility(item) + " ");
                builder.Append(item.GetGetMethod(true).IsStatic ? "static " : string.Empty);
                builder.Append(IsOver(type, item) + " ");
                builder.Append(GetReturn(item) + " ");
                builder.Append(GetMethodName(item) + " ");
                builder.Append("(" + GetParams(item) + ")");
                Console.WriteLine(builder.ToString());
            }

這裡不對泛型和陣列等複雜型別進行解析,也不輸出特性。

可以嘗試將 MyClass 換成 List<> 等型別進行測試。

輸出效果:

public  void TestA ()
public override void TestB ()
public override void TestC ()
public  void TestD ()
public  (Boolean,Boolean) TestE ()
public  String TestF<T> ( T t )
public  String TestG (in String a ,ref String aa ,out String b , String c = 666 )
public  String TestH (params String[] d )

完整程式碼已上傳到碼雲,點選檢視 解析方法與引數 。

2.1.5 建構函式

建構函式的話,沒有返回型別,也沒有重寫,獲取引數方法的部分,

因為有很多跟 2.1.4 重複的程式碼,因此這裡不再贅述,程式碼已經上傳到碼雲,可以參考 解析建構函式 。

2.1.6 屬性

正常來說呢,這樣寫屬性是可以的,但是過多的修飾符對屬性來說是沒意義的。

    public class MyClass
    {
        public int a { get; set; }
        internal int b { get; set; }
        protected int c { get; set; }
        protected internal int d { get; set; }
        private int e { get; set; }
        public  static float f { get; set; } = 1;
    }

PropertyInfo 沒有像 FieldInfo 那麼豐富的判斷修飾符的屬性。

但是呢,獲取到屬性的方法,則可以獲取訪問修飾符。

獲取訪問修飾符

跟獲取方法的訪問修飾符一樣,稍微調整以下即可。

        public static string GetVisibility(PropertyInfo property)
        {
            MethodInfo method = property.GetGetMethod();
            return
                method.IsPublic ? "public" :
                method.IsPrivate ? "private" :
                method.IsAssembly ? "internal" :
                method.IsFamily ? "protected" :
                method.IsFamilyOrAssembly ? "protected internal" :
                null;
        }

獲取重寫關鍵字

            // virtual override  abstract new
            public static string IsOver(Type type, PropertyInfo property)
            {
                MethodInfo method = property.GetGetMethod(true);
                // 沒有相應的資訊,說明沒有使用以上關鍵字修飾
                if (!method.IsHideBySig)
                    return string.Empty;

                // 是否抽象方法
                if (method.IsAbstract)
                    return "abstract";

                // virtual、override、實現介面的方法
                if (method.IsVirtual)
                {
                    // 實現介面的方法
                    if (method.IsFinal)
                        return string.Empty;
                    // 沒有被重寫,則為 virtual
                    if (method.Equals(method.GetBaseDefinition()))
                        return "virtual";
                    else
                        return "override";
                }
                // new
                else
                {
                    // 如果是當前型別中定義的方法,則只是一個普通的方法
                    if (type == method.DeclaringType)
                        return string.Empty;

                    return "new";
                }
            }

解析屬性構造器

            // 解析屬性的構造器
            public static string GetConstructor(PropertyInfo property)
            {
                string str = "{ ";
                if (property.CanRead)
                    str += "get; ";
                if (property.CanWrite)
                    str += "set; ";
                str += "}";
                return str;
            }

反射是無法直接拿到屬性的預設值的,詳細請參考 https://www.ojit.com/article/3058539。

以上,測試程式碼,可以到碼雲檢視 解析屬性

2.1.7 事件

本節沿用 2.1.4 中解析方法的所有函式。

定義委託和事件如下

    public delegate void DeTest();
    public abstract class A
    {
        public abstract event DeTest TestA;
    }
    public abstract class B : A
    {
        public virtual event DeTest TestB;
        public event DeTest TestC;
    }
    public class MyClass : B
    {
        public override event DeTest TestA;
        public override event DeTest TestB;
        new public event DeTest TestC;
    }

解析事件過程

            Type type = typeof(MyClass);
            EventInfo[] events = type.GetEvents(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly | BindingFlags.Instance);
            foreach (var item in events)
            {
                MethodInfo method = item.GetAddMethod();
                StringBuilder builder = new StringBuilder();
                builder.Append(GetVisibility(method) + " ");
                builder.Append(method.IsStatic ? "static " : string.Empty);
                builder.Append(IsOver(type, method) + " ");
                builder.Append("event ");
                builder.Append(item.EventHandlerType.Name + " ");
                builder.Append(item.Name + ";");
                Console.WriteLine(builder.ToString());
            }

解析過程是非常簡單的。

2.1.8 索引器

我們定義一個型別和索引器如下

    public class MyClass
    {
        private string[] MyArray;
        public MyClass()
        {
            MyArray = new string[] { "a", "b", "c", "d", "e" };
        }
        // 這裡不處理 search
        public string this[int index,string search]
        {
            get
            {
                return MyArray[index];
            }
            set
            {
                MyArray[index] = value;
            }
        }
    }

索引器在編譯時,會生成屬性和方法,所以使用反射獲取屬性時,會把索引器生成的屬性包含在內。

構造器會自動生成一個 public string Item { get; set; } 的屬性。

本節使用 2.1.6 中解析屬性的程式碼。

將屬性獲取方法優化如下,會區分輸出型別中的屬性和構造器。

            Type type = typeof(MyClass);
            PropertyInfo[] properties = type.GetProperties(BindingFlags.Public | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.DeclaredOnly | BindingFlags.Instance);

            foreach (PropertyInfo item in properties)
            {
                StringBuilder builder = new StringBuilder();
                builder.Append(GetVisibility(item) + " ");
                builder.Append(item.GetGetMethod(true).IsStatic ? "static " : string.Empty);
                builder.Append(IsOver(type, item) + " ");
                builder.Append(item.PropertyType + " ");
                if (item.Name == "Item")
                {
                    builder.Append("this[");
                    ParameterInfo[] paras = item.GetIndexParameters();
                    int length = paras.Length - 1;
                    for (int i = 0; i <= length; i++)
                    {
                        builder.Append(paras[i].ParameterType.Name + " " + paras[i].Name);

                        if (i < length)
                            builder.Append(",");
                    }
                    builder.Append("]");
                }
                else
                {
                    builder.Append(item.Name + " ");
                    builder.Append(GetConstructor(item));
                }

                Console.WriteLine(builder.ToString());
            }

2.1.9 獲取特性

型別、方法、屬性、欄位等,都可以使用特性修飾,我們要通過反射獲取特性後,還要將特性結果還原出程式設計師寫程式碼時設定的值。

程式碼如下

        /// <summary>
        /// 解析輸出型別、方法、屬性、欄位等特性
        /// </summary>
        /// <param name="attrs"></param>
        /// <returns></returns>
        public static string[] GetAttrs(IList<CustomAttributeData> attrs)
        {
            List<string> attrResult = new List<string>(); ;
            foreach (var item in attrs)
            {
                Type attrType = item.GetType();
                string str = "[";
                str += item.AttributeType.Name;
                // 建構函式中的值
                IList<CustomAttributeTypedArgument> customs = item.ConstructorArguments;
                // 屬性的值
                IList<CustomAttributeNamedArgument> arguments = item.NamedArguments;

                // 沒有任何值
                if (customs.Count == 0 && arguments.Count == 0)
                {
                    attrResult.Add(str + "]");
                    continue;
                }
                str += "(";
                if (customs.Count != 0)
                {
                    str += string.Join(",", customs.ToArray());
                }
                if (customs.Count != 0 && arguments.Count != 0)
                    str += ",";

                if (arguments.Count != 0)
                {
                    str += string.Join(",", arguments.ToArray());
                }
                str += ")";
                attrResult.Add(str);
            }
            return attrResult.ToArray();
        }

呼叫:

            Type type = typeof(List<>);
            string[] list = GetAttrs(type.GetCustomAttributesData());
            foreach (var item in list)
            {
                Console.WriteLine(item);
            }

呼叫時,將 Type 改成 MethodInfo 等。

輸出:

[SerializableAttribute]
[DebuggerDisplayAttribute("Count = {Count}")
[NullableAttribute((Byte)0)
[DebuggerTypeProxyAttribute(typeof(System.Collections.Generic.ICollectionDebugView`1))
[NullableContextAttribute((Byte)1)
[DefaultMemberAttribute("Item")
[TypeForwardedFromAttribute("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")

2.2 委託

這裡使用 2.1.4 中,解析方法的程式碼。

委託中,會有很多個方法,其中有個 invoke 方法,對應定義委託時的各種資訊。

        /// <summary>
        /// 解析委託,包括巢狀型別中的委託
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        public static string GetDelegateInfo(Type type)
        {
            if (!type.IsSubclassOf(typeof(Delegate)))
                return null;

            string str = "";
            MethodInfo method = type.GetMethod("Invoke");

            if (type.IsNested)
                str += GetVisibility(method);
            else
                str += (type.IsPublic ? "public" : "internal") + " ";
            str += type.IsSealed && type.IsAbstract ? "static " : string.Empty;
            str += "delegate ";
            str += GetReturn(method) + " ";
            str += type.Name;
            str += "(";
            str += GetParams(method);
            str += ")";

            return str;
        }

2.3 介面

上面已經解析類、抽象類、委託等,可以使用同樣的方法解析介面,然後接著解析介面的屬性、方法。

這裡不再贅述。

2.4 可空型別

判斷一個型別是否為可空型別時,可以先判斷是否為泛型。

可空型別和泛型方法都可以使用 IsGenericType 屬性判斷。

GetGenericTypeDefinition 方法可以獲取泛型未繫結引數的版本。

最後判斷型別是否為 typeof(Nullable<>) ,即可完成整體解析。

        /// <summary>
        /// 獲取可空型別名稱
        /// </summary>
        /// <returns></returns>
        public static string GetAbleNullName(Type type)
        {
            if (!type.IsGenericType)
                return type.Name;

            if (type.GetGenericTypeDefinition() == typeof(Nullable<>))
            {
                Type nullType = type.GetGenericArguments().FirstOrDefault();
                return nullType.Name + "?";
            }
            return type.Name;
        }