C#学习之路(二):反射(Reflection)

1、什么是反射(Reflection)

(1)类比

首先用现实生活中的例子做个类比,大家体检时做B超,仪器发射B型超声波,穿过肚皮遇到内脏并形成“回音”,设备将“回音”处理再以影像的方式显示在屏幕上,就可以看到内脏的情况了。

(2)反射机制

反射是一个广泛概念,在计算机领域,反射是由Smith在1982年首次提出的,主要是指程序可以访问、检测和修改它本身状态或行为的一种能力,已经被多种程序语言所实现。

(3).NET Framework中的反射

C# 是运行于 .NET Framework 和 .NET Core 之上的程序设计语言,所以C#实现的实际上是它们的反射机制,本文介绍 .NET Framework 中的反射。

.NET Framework 的封装有程序集(Assembly)、模块(Module)、类型(Type)几个层次,程序集包含模块,模块包含类型、类型包含成员。

程序集(Assembly)是经由编译器编译得到的,供CLR进一步编译执行的那个中间产物,在WINDOWS系统中,它一般表现为.dll或者是.exe的格式,但是要注意,它们跟普通意义上的WIN32可执行程序是完全不同的东西,程序集必须依靠CLR才能顺利执行。程序集是 .NET Framework 应用程序的主要构造块。所有托管类型和资源都包含在某个程序集内,并被标记为只能在该程序集的内部访问,或者被标记为可以从其他程序集中的代码访问。

模块(Module)是指托管模块,一个程序集可以有多个模块和资源文件组成,不过一般我们写的程序集中都用一个模块,模块中包含IL中间代码和元数据。

类型(Type)是一个特殊的类,是反射的中心。 当反射提出请求时,公共语言运行时为已加载的类型创建 Type。可使用Type对象的方法、字段、属性和嵌套类来查找该类型的任何信息。Type不等于Class。

2、反射的典型用法

反射提供封装程序集、模块和类型的对象。可以使用反射动态地创建类型的实例,将类型绑定到现有对象,或从现有对象中获取类型,然后,可以调用类型的方法或访问其字段和属性。

反射的典型用法如下:

(1)使用 Assembly 来定义和加载程序集,加载程序集清单中列出的模块,以及在此程序集中定位一个类型并创建一个它的实例。

using System;
using System.Reflection;

public class LickDog
{
    public static void Main()
    {
        //获取正在运行的程序集
        Assembly assembly = Assembly.GetExecutingAssembly();
        Console.WriteLine(assembly);

        //列出程序集中的模块
        Module[] module = assembly.GetModules();
        foreach (Module each in module)
        {
            Console.WriteLine(each);
        }

        //列出程序集中的类型
        Type[] type = assembly.GetTypes();
        foreach (Type each in type)
        {
            Console.WriteLine(each);
        }
        Console.ReadKey();
    }
}

运行结果:

demo, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
demo.exe
LickDog

第一行为程序集及其参数,第二行为程序集中的模块,第三行为程序集中的类型。

(2)使用 Module 发现信息,如包含模块的程序集和模块中的类型。 还可以获取所有全局方法或模块上定义的其它特定的非全局方法。

using System;
using System.Reflection;

public class Her
{
    private double weight;

    private bool loseWeight()
    {
        return true;
    }

    public void haveLunch()
    {

    }
}

public class LickDog
{
    private double height;

    public static void Main()
    {
        Assembly assembly = Assembly.GetExecutingAssembly();
        foreach (Module module in assembly.GetModules())
        {
            //模块信息
            Console.WriteLine(module);
            //模块包含的程序集
            Console.WriteLine(module.Assembly);

            foreach (Type type in module.GetTypes())
            {
                //模块中的类型
                Console.WriteLine(type);
            }
        }
        Console.ReadKey();
    }
}

运行结果:

demo.exe
demo, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
Her
LickDog

第一行为模块,第二行是包含模块的程序集,第三第四行为模块中的类型。

(3)使用 ConstructorInfo 发现信息,如名称、参数、访问修饰符(如 public 或 private)和构造函数的实现详细信息(如 abstract 或 virtual)。 使用 Type 的 GetConstructors 或 GetConstructor 方法来调用特定构造函数。

using System;
using System.Reflection;

public class LickDog
{
    private string name;

    public LickDog(string name)
    {
        this.name = name;
    }

    public static void Main()
    {
        Assembly assembly = Assembly.GetExecutingAssembly();
        foreach (Type type in assembly.GetTypes())
        {
            foreach (ConstructorInfo cons in type.GetConstructors())
            {
                //类型
                Console.WriteLine(type);
                //构造方法信息
                Console.WriteLine(cons);
            }
        }
        Console.ReadKey();
    }
}

运行结果:

LickDog
Void .ctor(System.String)

构造方法名默认为.ctor。

(4)使用 MethodInfo 发现信息,如名称、返回类型、参数、访问修饰符(如 public 或 private)和方法的实现详细信息(如 abstract 或 virtual)。 使用 Type 的 GetMethods 或 GetMethod 方法来调用特定方法。

using System;
using System.Reflection;

public class Her
{
    private double weight;

    private Her(double weight)
    {
        this.weight = weight;
    }
    private double haveLunch(double food)
    {
        weight += food;
        return weight;
    }
    public bool loseWeight()
    {
        return true;
    }
}

public class LickDog
{
    private bool loveHer()
    {
        return true;
    }
    public static void Main()
    {
        Assembly assembly = Assembly.GetExecutingAssembly();
        foreach (Module module in assembly.GetModules())
        {
            //模块包含的程序集
            Console.WriteLine(module.Assembly);

            foreach (Type type in module.GetTypes())
            {
                //模块中包含的类型
                Console.WriteLine(type);

                foreach (MethodInfo method in type.GetMethods())
                {
                    //模块中的public方法信息
                    Console.WriteLine(method);
                }
            }
        }
        Console.ReadKey();
    }
}

运行结果:

demo, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
Her
Boolean loseWeight()
System.String ToString()
Boolean Equals(System.Object)
Int32 GetHashCode()
System.Type GetType()
LickDog
Void Main()
System.String ToString()
Boolean Equals(System.Object)
Int32 GetHashCode()
System.Type GetType()

demo为包含模块的程序集,Her和LickDog为模块中的类型,loseWeight和Main是相应类型中的公有方法,这里要注意,无论本类型或其他类型,私有方法(loveHer和haveLunch)默认无法通过反射查询(需要授予一定权限)。由于这些类都继承于Object类,ToString、Equals、GetHashCode、GetType是其父类的方法。

(5)使用 FieldInfo 发现信息,如名称、访问修饰符(如 public 或 private)和一个字段的实现详细信息 (如 static);并获取或设置字段值。

using System;
using System.Reflection;

public class Her
{
    public string name;

    private double weight;
}

public class LickDog
{
    public double weight;

    private double height;

    public static void Main()
    {
        Assembly assembly = Assembly.GetExecutingAssembly();
        foreach (Type type in assembly.GetTypes())
        {
            //类型信息
            Console.WriteLine(type);

            foreach (FieldInfo field in type.GetFields())
            {
                //字段信息
                Console.WriteLine(field);
            }
        }
        Console.ReadKey();
    }
}

运行结果:

Her
System.String name
LickDog
Double weight

通过反射查询类型中的公有字段。

(6)使用 EventInfo 发现信息(如名称、事件处理程序的数据类型、自定义特性、声明类型以及事件的反射的类型),并添加或删除事件处理程序。

using System;
using System.Reflection;

public class Her
{
    //声明委托
    public delegate void BoilerLogHandler(string status);

    //声明事件
    public event BoilerLogHandler BoilerEventLog;
}

public class LickDog
{
    public static void Main()
    {
        Assembly assembly = Assembly.GetExecutingAssembly();
        foreach (Type type in assembly.GetTypes())
        {
            //类型信息
            Console.WriteLine(type);

            foreach (EventInfo eventInfo in type.GetEvents())
            {
                //事件信息
                Console.WriteLine(eventInfo);
            }
        }
        Console.ReadKey();
    }
}

运行结果:

Her
BoilerLogHandler BoilerEventLog
LickDog
Her+BoilerLogHandler

第二行第四行为事件信息。

(7)使用 PropertyInfo 发现信息(如名称、数据类型、声明类型,反射的类型和属性的只读或可写状态),并获取或设置属性值。

using System;
using System.Reflection;

public class Her
{
    private string name;
    
    //声明属性
    public string Name
    {
        get
        {
            return name;
        }
        set
        {
            name = value;
        }
    }
}

public class LickDog
{
    public static void Main()
    {
        Assembly assembly = Assembly.GetExecutingAssembly();
        foreach (Type type in assembly.GetTypes())
        {
            //类型信息
            Console.WriteLine(type);

            foreach (PropertyInfo proper in type.GetProperties())
            {
                //属性信息
                Console.WriteLine(proper);
            }
        }
        Console.ReadKey();
    }
}

运行结果:

Her
System.String Name
LickDog

第二行为属性信息。

(8)使用 ParameterInfo 发现信息,如参数的名称、数据类型、参数是输入参数还是输出参数以及参数在方法签名中的位置。

using System;
using System.Reflection;

public class LickDog
{
    private string name;

    public LickDog(string name)
    {
        this.name = name;
    }

    public static void Main()
    {
        Assembly assembly = Assembly.GetExecutingAssembly();
        foreach (Type type in assembly.GetTypes())
        {
            foreach (ConstructorInfo cons in type.GetConstructors())
            {
                foreach (ParameterInfo para in cons.GetParameters())
                {
                    //类型
                    Console.WriteLine(type);
                    //构造方法信息
                    Console.WriteLine(cons);
                    //参数信息
                    Console.WriteLine(para);
                }
            }
        }
        Console.ReadKey();
    }
}

运行结果:

LickDog
Void .ctor(System.String)
System.String name

(9)使用 CustomAttributeData 在于应用程序域的仅反射上下文中工作时发现有关自定义特性的信息。 CustomAttributeData 使你能够检查特性,而无需创建它们的实例。

using System;
using System.Reflection;

// 描述如何使用一个自定义特性
[AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = true)]

//自定义特性
public class MyAttribute : Attribute
{
    private string incident; //事件
    private string data;     //日期
    public string Incident
    {
        get { return incident; }
        set { incident = value; }
    }
    public string Data
    {
        get { return data; }
        set { data = value; }
    }
    public MyAttribute(string incident)
    {
        this.incident = incident;
    }
}

//实例化自定义
[My("First meet", Data = "2020-01-30")]
[My("Second meet", Data = "2020-03-16")]
class Init { }

//访问特性
public class LickDog
{
    public static void Main()
    {
        Type init = typeof(Init);
        var myAttribute = CustomAttributeData.GetCustomAttributes(init);
        foreach (CustomAttributeData cus in myAttribute)
        {
            Console.WriteLine(cus);
        }
        Console.ReadKey();
    }
}

运行结果:

[MyAttribute(“First meet”, Data = “2020-01-30”)]
[MyAttribute(“Second meet”, Data = “2020-03-16”)]

用CustomAttributeData访问自定义特性。

3、反射安全注意事项

(1)说明

从 .NET Framework 4 开始,只有受信任的代码才能使用反射来访问关键安全成员。而且,只有受信任的代码才能使用反射访问无法由已编译代码直接访问的非公共成员。最后,使用反射访问关键安全成员的代码必须具有关键安全成员要求的任何权限,就像编译的代码一样具有一定的权限,代码可以使用反射来执行以下类型的访问:
● 访问不是安全关键的公共成员。
● 若这些成员不是安全关键,则访问可进入编译代码的非公共成员。此类非公共成员的示例包括:
○○ 调用代码的基础类的受保护成员。(在反射中,这称为系列级访问权限。)
○○ 调用代码的程序集中的 internal 成员。(在反射中,这称为程序集级别的访问。)
○○ 包含调用代码的类的其他实例的私有成员。

(2)访问通常不可访问的成员

根据公共语言运行时的可访问性规则,若要使用反射来调用无法访问的成员,你的代码必须获得以下两个权限之一:
● 若要允许代码调用任何非公共成员:代码必须获得带 ReflectionPermissionFlag.MemberAccess 标志的 ReflectionPermission。
● 要允许代码调用任何非公共成员,只要包含调用成员的程序集的授予集与包含调用代码的程序集的授予集相同或与其子集相同:你的代码必须授予带 ReflectionPermissionFlag.RestrictedMemberAccess 标志的 ReflectionPermission。

using System;
using System.Reflection;

public class Her
{
    public string name;

    private double weight = 1000;

    private double haveLunch(double food)
    {
        weight += food;
        return weight;
    }
    public bool loseWeight()
    {
        return true;
    }
}

public class LickDog
{
    private double height = 0.0;

    private bool loveHer()
    {
        return true;
    }

    public static void Main()
    {
        Assembly assembly = Assembly.GetExecutingAssembly();
        foreach (Type type in assembly.GetTypes())
        {
            //类型信息
            Console.WriteLine(type);

            Console.WriteLine("方法");
            foreach (MethodInfo method in type.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance))
            {
                Console.WriteLine(method);
            }

            Console.WriteLine("字段");
            foreach (FieldInfo field in type.GetFields(BindingFlags.NonPublic | BindingFlags.Instance))
            {
                Console.WriteLine(field);
            }
            Console.WriteLine("  ");
        }

        Her her = new Her();
        Type herType = typeof(Her);
        FieldInfo f = herType.GetField("weight", BindingFlags.NonPublic | BindingFlags.Instance);
        Console.WriteLine("Her weight:"+f.GetValue(her));
        Console.ReadKey();
    }
}

运行结果:

Her
方法
Double haveLunch(Double)
Void Finalize()
System.Object MemberwiseClone()
字段
Double weight
———————————————————————————————————
LickDog
方法
Boolean loveHer()
Void Finalize()
System.Object MemberwiseClone()
字段
Double height
———————————————————————————————————
Her weight:1000

这段代码成功访问到了之前无法访问到的私有方法和私有字段。

首先这段代码符合条件二,其次与之前相比,这段代码在获取信息的时候加了一段BindingFlags.NonPublic | BindingFlags.Instance,查看文档得知,BuildingFlags是一个枚举类型,是指定控制绑定以及通过反射执行成员和类型搜索的方式的标记。Instance字段指定实例成员要包括在搜索中,NonPublic字段指定非公共成员要包括在搜索中。我们就是通过此方式反射访问到了私有方法和字段,再通过字段名访问到实例化对象私有字段的值。