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对象来的对应他们,如下面的图示:

C#的反射(一)

简单看一下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之间的关系

C#的反射(一)

所以下面的代码,在遍历派生类的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和构造函数的参数。当不存在这个构造函数的时候,就会抛出错误。

C#的反射(一)

方法二 调用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,那么就是报出下面的错误:说明存在二义性,也就是说找不到对应使用哪个来构造。

C#的反射(一)

同样的,如果我使用方法一 Activator.CreateInstance 去创建对象,会出现下面的问题:找不到对应的构造函数。

C#的反射(一)

但是采用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.范型的实例化

泛型分为封闭型和未封闭型,对于封闭类型的泛型是可以通过反射进行实例化的,而未封闭的泛型不能实例化。如下图所示:

C#的反射(一)

封闭式的泛型和未绑定的泛型是可以相互转换的。

①未绑定的泛型可以通过 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#》 

C#的反射(一)

上一篇:创建dynamics CRM client-side (二) - Client API


下一篇:C# 轮流展示照片