C#的反射(一)


1.什么是元數據(MetaData)和反射(reflection)

一般情況下我們的程序都在處理數據的讀、寫、操作和展示。但是有些程序操作的數據不是數字、文本、圖片,而是程序和程序類型本身的信息。

①元數據是包含程序以及類型信息的數據,它保存在程序的程序集當中。

②程序在運行的時候,可以查看其他程序集或者其本身的元數據。這個行為就是反射。

2.Type

BCL聲明了一個Type類型(它是抽象類),用來包含類型的特性。使用這個類的對象能讓我們獲取程序使用的類型的信息。

由於Type是抽象類,所以它不能被實例化。而是在運行時,CLR創建從Type(RuntimeType)派生的類型的實例。當我們要訪問這些實例的時候,CLR不會返回派生類的引用而是返回Type基類的引用。

關於Type有如下重要的點:

①對於程序每一個需要用到的類型,CLR會穿件一個包含這個類型信息的Type類型的對象(真實的是上面說的派生的類型的實例)。

②程序中用到的每一個類型都會關聯到獨立的Type類的對兩個象。

③無論創建的類型有多少個實例,只有一個Type對象會關聯到所有這些實例。就像下面的圖表示的一樣。創建了一個OtherClass的實例oc、以及兩個MyClass的實例mc1和mc2,但是在堆上都只會有一個Type對象來的對應他們,如下面的圖示:

簡單看一下Type這個類型,里面可以看到如下的一些方法和屬性。

官方文檔更全面哦: https://docs.microsoft.com/zh-cn/dotnet/api/system.type?view=netframework-4.8

 

3.學習如何獲取一個Type類對象

方法一:通過GetType方法

object類型包含了一個GetType方法,它可以用來返回事例的Type對象引用。由於所有的類都是繼承自object類型,所以所有的類都可以調用GetType來獲得Type類型對象的引用。下面的圖很好的說明了,基類、派生類和object之間的關系

所以下面的代碼,在遍歷派生類的Field的時候才能,把基類的也輸出出來。

//基類
class BaseClass
{
    public int BaseField = 0;
}

//派生類
class DerivedClass : BaseClass
{
    public int DerivedField = 0;
}

class Program
{
    static void Main(string[] args)
    {
        var bc = new BaseClass();
        var dc = new DerivedClass();
        BaseClass[] bca = new BaseClass[] { bc, dc };
        foreach(var v in bca)
        {
            //獲取類型
            Type t = v.GetType();
            Console.WriteLine("Object Type: {0}", t.Name);
            //獲取類中的字段
            FieldInfo[] fi = t.GetFields();
            foreach (var f in fi)
                Console.WriteLine("     Field:{0}", f.Name);
            Console.WriteLine();
        }
        Console.WriteLine("End!");
        Console.ReadKey();
    }
}

結果:

Object Type: BaseClass
     Field:BaseField
Object Type: DerivedClass
     Field:DerivedField
     Field:BaseField
End!

方法二:還可以通過typeof()方法來獲取一個類型的Type對象引用。例如下面的代碼:

 Type t = typeof(DerivedClass);

此外我們可以根據程序集來獲取程序集內的類型

//通過程序集獲取類型
var baseType = Assembly.GetExecutingAssembly().GetType("TestDemo.BaseClass");
var derivedType = Assembly.GetExecutingAssembly().GetType("TestDemo.DerivedClass");

4.常用的操作

結合GetType和typeof操作,可以做很多事情。

獲取數組類型

static void Main(string[] args)
{
    var intArray = typeof(int).MakeArrayType();
    var int3Array = typeof(int).MakeArrayType(3);

    Console.WriteLine($"是否是int 數組 intArray == typeof(int[]) :{intArray == typeof(int[]) }");
    Console.WriteLine($"是否是int 3維數組 intArray3 == typeof(int[]) :{int3Array == typeof(int[]) }");
    Console.WriteLine($"是否是int 3維數組 intArray3 == typeof(int[,,]):{int3Array == typeof(int[,,]) }");

    //數組元素的類型
    Type elementType = intArray.GetElementType();
    Type elementType2 = int3Array.GetElementType();

    Console.WriteLine($"{intArray}類型元素類型:{elementType }");
    Console.WriteLine($"{int3Array}類型元素類型:{elementType2 }");

    //獲取數組的維數
    var rank = int3Array.GetArrayRank();
    Console.WriteLine($"{int3Array}類型維數:{rank }");
    Console.ReadKey();
}

如上面的例子,

MakeArrayType() 可以用來獲取數組類型,有一個參數是數組的維數

GetElementType() 可以用來獲取數組元素的類型

GetArrayRank() 可以獲取數組的維數

獲取嵌套類型

public class Class
{
    public class Student
    {
        public string Name { get; set; }
    }
}

class Program
{
    static void Main(string[] args)
    {
        #region 嵌套類型
        var classType = typeof(Class);

        foreach (var t in classType.GetNestedTypes())
        {
            Console.WriteLine($"NestedType ={t}");
            //獲取一個值,該值指示 System.Type 是否聲明為公共類型。
            Console.WriteLine($"{t}訪問 {t.IsPublic}");
            //獲取一個值,通過該值指示類是否是嵌套的並且聲明為公共的。
            Console.WriteLine($"{t}訪問 {t.IsNestedPublic}");
        }

        Console.ReadKey();
        #endregion
    }
}

輸出:

NestedType =TestDemo.Class+Student
TestDemo.Class+Student訪問 False
TestDemo.Class+Student訪問 True

獲取類型名稱

Type里面具有NameSpace、Name和FullName屬性。一般FullName是兩者的組合。但是對於嵌套類型和封閉式泛型不成立。可以參考下面的demo

static void Main(string[] args)
{
    #region 獲取名稱
    var type = typeof(Class);
    Console.WriteLine($"\n------------一般類型-------------");
    PrintTypeName(type);

    //嵌套類型
    Console.WriteLine($"\n------------嵌套類型-------------");
    foreach (var t in type.GetNestedTypes())
    {
        PrintTypeName(t);
    }

    var type2 = typeof(Dictionary<,>);            //非封閉式泛型
    var type3 = typeof(Dictionary<string, int>);  //封閉式泛型

    Console.WriteLine($"\n------------非封閉式泛型-------------");
    PrintTypeName(type2);
    Console.WriteLine($"\n------------封閉式泛型-------------");
    PrintTypeName(type3);
    Console.ReadKey();
    #endregion

}

private static void PrintTypeName(Type t)
{
    Console.WriteLine($"NameSpace: {t.Namespace}");
    Console.WriteLine($"Name :{t.Name}");
    Console.WriteLine($"FullName: {t.FullName}");
}

結果:

------------一般類型-------------
NameSpace: TestDemo
Name :Class
FullName: TestDemo.Class

------------嵌套類型-------------
NameSpace: TestDemo
Name :Student
FullName: TestDemo.Class+Student
NameSpace: TestDemo
Name :Teacher
FullName: TestDemo.Class+Teacher

------------非封閉式泛型-------------
NameSpace: System.Collections.Generic
Name :Dictionary`2
FullName: System.Collections.Generic.Dictionary`2

------------封閉式泛型-------------
NameSpace: System.Collections.Generic
Name :Dictionary`2
FullName: System.Collections.Generic.Dictionary`2[
[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],
[System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]

獲取基類類型和接口類型

var base1 = typeof(System.String).BaseType;
var base2 = typeof(System.IO.FileStream).BaseType;
var base3 = typeof(DerivedClass).BaseType;

Console.WriteLine($"base1 :{base1.Name}");
Console.WriteLine($"base2 :{base2.Name}");
Console.WriteLine($"base3 :{base3.Name}");

foreach (var iType in typeof(Guid).GetInterfaces())
{
    Console.WriteLine($"iType :{iType.Name}");
}

輸出:

base1 :Object
base2 :Stream
base3 :BaseClass
iType :IFormattable
iType :IComparable
iType :IComparable`1
iType :IEquatable`1

此外Type還有兩個方法:

我們在判斷某個實例對象是否是某個類型的時候,經常使用 is語句。

Type中的方法 IsInstanceOfType 其實和is是等價的。

var baseClassObject = new BaseClass();
var check1 = baseClassObject is BaseClass;
var check2 = base3.IsInstanceOfType(baseClassObject);
Console.WriteLine($"使用is判斷類型是否相同 :{check1}");  //結果True
Console.WriteLine($"使用IsInstanceOfType類型是否相同 :{check2 }"); //結果True 

返回結果都是True的。

還有一個是 IsAssignableFrom ,它的作用是確定指定類型的實例是否可以分配給當前類型的實例。

var base4 = typeof(BaseClass); //baseClass的實例
var baseClassObject = new BaseClass();
var derivedClassObject = new DerivedClass();
var classObject = new Class();
var checkResult1 = base4.IsAssignableFrom(baseClassObject.GetType()); //判斷BaseClass類型是否可以分配給BassClass類型
var checkResult2 = base4.IsAssignableFrom(derivedClassObject.GetType());  //判斷DerivedClass類型是否可以分配給BassClass類型
var checkResult3 = base4.IsAssignableFrom(classObject.GetType()); //判斷Class類型是否可以分配給BassClass類型
Console.WriteLine($"使用IsAssignableFrom類型是否和接受的類型一致 :{checkResult1}");   //True
Console.WriteLine($"使用IsAssignableFrom類型是否和接受的類型一致 :{checkResult2}");   //True 
Console.WriteLine($"使用IsAssignableFrom類型是否和接受的類型一致 :{checkResult3}");  //False

實例化類型

I. 有兩種方法可以動態的實例化類型。

方法一 通過靜態的 Activator.CreateInstance()方法創建,它有多個重載函數。

var dateTime1 = (DateTime)Activator.CreateInstance(typeof(DateTime),2019,6,19);
var dateTime2 = (DateTime)Activator.CreateInstance(typeof(DateTime), 2019,6,19,10,10,10);
Console.WriteLine($"DateTime1: {dateTime1}"); //DateTime1: 2019/6/19 0:00:00
Console.WriteLine($"DateTime2: {dateTime2}"); //DateTime2: 2019/6/19 10:10:10

一般我們像上面一樣都是傳一個Type和構造函數的參數。當不存在這個構造函數的時候,就會拋出錯誤。

方法二 調用ConstructInfo對象上面的Invoke方法,ConstructInfo對象是通過調用類型(高級環境)上的GetConstructor方法獲取的。

先分析一下場景,例如我有下面這樣的一個類型:

public class InvokeClass
{
    private string _testString;
    private long _testInt;
    
    public InvokeClass(string abc)
    {
        _testString = abc;
    }
    public InvokeClass(StringBuilder abc)
    {
        _testString = abc.ToString();
    } 
    
   public InvokeClass(string abc,long def)
    {
        _testString = abc;
        _testInt = def;
    }    
}

存在兩個構造函數,一個傳入的是string類型,一個傳入的是StringBuilder類型,此時如果我通過new 的方式去創建一個對象,並傳入構造函數為null,那么就是報出下面的錯誤:說明存在二義性,也就是說找不到對應使用哪個來構造。

同樣的,如果我使用方法一 Activator.CreateInstance 去創建對象,會出現下面的問題:找不到對應的構造函數。

但是采用ConstructInfo的方式就可以指定對應的構造函數了。類似如下代碼

//找到一個參數為string的構造函數
var constructorInfo = typeof(InvokeClass).GetConstructor(new[] { typeof(string)});      
//使用該構造函數傳入一個null參數      
var obj4 = (InvokeClass)constructorInfo.Invoke(new object[] { null });

還可以結合查詢來找到對應的構造函數

//獲取所有的構造函數
var constructorInfoArray = typeof(InvokeClass).GetConstructors();
//過濾一次,獲取所有兩個參數的構造函數
var constructorInfoArray2 = Array.FindAll(constructorInfoArray, x => x.GetParameters().Length == 2);
//最后找的第二個參數是long類型的構造函數
var constructorInfo2 = Array.Find(constructorInfoArray2, x => x.GetParameters()[1].ParameterType == typeof(long));
//如果存在,就創建對象
if (constructorInfo2 != null)
{
    var obj5 = (InvokeClass)constructorInfo2.Invoke(new object[] { "abc", 123 });
}

動態構造對象的缺點就是慢,簡單對比一下,采用反射和new創建100萬個對象,耗時對比還是比較明顯的。

var sw = new Stopwatch();
sw.Start();
for (int i = 0; i < 100000; i++)
{
    var obj3 = (InvokeClass)Activator.CreateInstance(typeof(InvokeClass), "abc", 123);
}
sw.Stop();
Console.WriteLine($"時間:{sw.ElapsedMilliseconds}ms");

var sw2 = new Stopwatch();
sw2.Start();
for (int i = 0; i < 100000; i++)
{
    var obj = new InvokeClass("abc", 123);

}
sw2.Stop();
Console.WriteLine($"時間:{sw2.ElapsedMilliseconds}ms");

輸出:

時間:280ms
時間:1ms

 

II. 實例化委托

動態創建靜態方法和實例方法的委托傳入的參數不太一樣,使用的是CreateDelegate的重載,可以參考下面的例子

/// <summary>
///  創建指定類型的委托,該委托表示要對指定的類實例調用的指定實例方法。
/// </summary>
/// <param name="type">要創建的委托的 System.Type</param>
/// <param name="target"> 類實例,對其調用 method</param>
/// <param name="method">委托要表示的實例方法的名稱</param>
/// <returns></returns>
public static Delegate CreateDelegate(Type type, object target, string method);

/// <summary>
///  創建指定類型的委托,該委托表示指定類的指定靜態方法。
/// </summary>
/// <param name="type">要創建的委托的 System.Type</param>
/// <param name="target">  表示實現 method 的類的 System.Type</param>
/// <param name="method"> 委托要表示的靜態方法的名稱。</param>
/// <returns></returns>
public static Delegate CreateDelegate(Type type, Type target, string method);

例如:

class Program
{    
    public static int StaticSum(int a, int b)   {
        return a + b;
    }

    public int InstanceSum(int a, int b)
    {
        return a + b;
    }

    //創建一個委托
    delegate int delegateOperate(int a, int b);
    static void Main(string[] args)
    {
        #region 實例化委托
        //靜態方法的委托
        Delegate staticD = Delegate.CreateDelegate(typeof(delegateOperate), typeof(Program), "StaticSum");
        //實例方法的委托
        Delegate instanceD = Delegate.CreateDelegate(typeof(delegateOperate), new Program(), "InstanceSum");
        
        Console.WriteLine($"staticD:{staticD.DynamicInvoke(1,2)}");
        Console.WriteLine($"instanceD:{instanceD.DynamicInvoke(10,20)}");
        #endregion
        Console.ReadKey();
    }        
}

III.范型的實例化

泛型分為封閉型和未封閉型,對於封閉類型的泛型是可以通過反射進行實例化的,而未封閉的泛型不能實例化。如下圖所示:

封閉式的泛型和未綁定的泛型是可以相互轉換的。

①未綁定的泛型可以通過 MakeGenericType 變成封閉的

②封閉的可以通過GetGenericTypeDefinition 獲取未綁定的類型。

class Program
{
    static void Main(string[] args)
    {
        Type closed = typeof(List<int>);
        Type unBound = typeof(List<>);

        //轉換
        var newClosed = unBound.MakeGenericType(typeof(int));
        var newUnBound = closed.GetGenericTypeDefinition();

        Console.WriteLine($"List<int> 類型{closed}");
        Console.WriteLine($"List<> 類型{unBound}");
        Console.WriteLine($"List<> MakeGenericType執行后 類型{newClosed}");
        Console.WriteLine($"List<int> GetGenericTypeDefinition執行后 類型{newUnBound}");
    }
}

 

參考: 《C#圖解教程》、《果殼中的C#》 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM