反射(reflection和reflection.emit)

本文详细介绍了反射的两大核心部分:一是发现并利用运行时对象信息,包括加载程序集、获取类型、成员信息及执行操作;二是动态生成代码,通过反射emit实现。文章通过实例演示了如何创建和操作类、方法、属性等,以及如何在运行时执行代码,展示了反射的强大功能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

    反射分为两部分:

1. 发现运行中对象的信息,调用其中的方法、属性值等,用到的是system.reflection命名空间下的类中的方法;

2. 在运行时动态产生代码,用到的是system.reflection.emit命名空间下的类中的方法。

 

第一部分:

首先,你必须在一个已知的类型或程序集上创建类Reflection.Assembly的一个实例,可以使用类Assembly的静态的方法:Load来实现的。该方法有很多版本,我们可以举两个例子:

  • Public Static Assembly Load(string):参数为程序集的名称,即命名空间,比如:Assembly myAssembly = Assembly.Load("System.Drawing");
  • Public Static Assembly LoadFrom(strig):参数为程序集的全路径,比如:Assembly myAssembly = Assembly.LoadFrom(@"C:\WINNT\Microsoft.NET\Framework\v1.1.4322\System.Drawing.dll");

也可以通过下面几种方法获取Assembly里的实例:

  • Assembly myAssembly = typeof(System.Data.DataRow).Assembly;
  • DataTable dt = new DataTable();    Assembly myAssembly = dt.GetType().Assembly;

接下来该介绍如何获取程序集内容的一些信息,比如类、方法、属性、字段等。

可以通过Type[] types = myAssembly.GetTypes();来获取类型数组,然后遍历type来获取其中的信息,比如:

Type[] types = myAssembly.GetTypes();

foreach(Type type in types)

{

    if(type.IsClass)

        {

            Console.WriteLine(type.Name);

        }

}

然后,获取到程序集中的类型后,我们可以对其中的类型进行筛选(搜索、过滤和查找)。

定义如下的一个类:

public class SomeClass
{

private int myPrvField1 = 15;

private string myPrvField2 = "Some private field";

public decimal myPubField1 = 1.03m;
 
public SomeClass()
{}
 
public SomeClass(int someValue)
{}
public SomeClass(int someValue,int someOtherValue)
{}
public void SomeMethod()
{}

public static void OtherMethod()
{}

publicstatic  void AnotherMethod()
{}

}

        1.  可以通过一些方法直接获取程序集中的构造函数(GetConstructor)、方法(GetMethod)、属性(GetProperty)和事件(GetEvent),比如:

  • Type[] ts = {typeof(Int32)}; ConstructorInfo ci = typeof(SomeClass).GetConstructor(ts);  该方法可以获取到带有一个int类型的参数的构造函数信息实例
  • MethodInfo mi = typeof(SomeClass).GetMethod("SomeMethod");  把方法名作为参数传递到该方法中,将获取到该方法的信息实例

        2. 可以通过BindingFlags来设置过滤条件,可以获取到公共的、私有的、受保护的、静态的等属性、方法或事件等信息,比如:

  • MethodInfo[] mis = typeof(SomeClass).GetMethods(BindingFlags.Public | BindingFlags.Static);该方法可以获取到SomeClass中公共的静态方法。

       3. 可以通过System.Type的一个抽象的方法FindMembers设置参数MemberType、BindingFlags等查找到程序集中想要的信息,比如:

  • MemberInfo[] memInfo = typeof(SomeClass).FindMembers(MemberTypes.Field,BindingFlags.NonPublic | BindingFlags.Instance,null,null);

foreach(MemberInfo m in memInfo)
{
fi 
= m as FieldInfo;
if(fi != null)
{
Console.WriteLine(
"{0} of value:{1}",fi.Name,fi.GetValue(ac));
}

}

该方法获取到了类实例中的私有字段并显示出它们的值。

另外,一旦你发现目标成员变量,就可以把它们转化为真正的FieldInfo对象,这样你就可以查找它们的值了。

最后,我们可以通过查找到的程序集中信息执行其中的一些操作(比如执行其中的一些方法)。这就是后期绑定的结果:在运行的时候定位并且执行一个类型(创建该类型的实例或者执行其方法,而此类型在你设计之初并不知晓)。

执行发现的代码的过程基本上要遵循以下几个基本的步骤:

  • 加载程序集
  • 找到你希望使用的类型或者类
  • 创建该类型(或者类)的一个实例
  •  找到你希望执行的该类型的某个方法
  • 得到该方法的参数
  • 调用该对象上的方法并且传递给它恰当的参数

一旦找到要找的类型,就可以使用System.Activator创建此类型的一个实例。使用Activator类的方法CreateInstance(允许指定你想要创建的对象,并且可选择的参数会应用到该对象的构造器上)创建一个类的实例,比如:object obj = System.Activator.CreateInstance(typeof(SomeClass));。

在使用CreateInstance创建类的实例时,可以通过带参数的构造方法来获取类的实例。首先是查找带参数的构造器,只要确定了构造器,就可以使用类ConstructorInfo的方法GetParameters得到其参数。GetParameters会返回一个ParameterInfo对象数组,它将会帮助你确定参数的顺序,参数的名称以及参数的类型。随后就可以建立参数值的数组,然后把它传递给CreateInstance方法。比如:

假定你拥有SomeClass的构造器的一个引用(名称为ci):

ParameterInfo[] pi = ci.GetParameters();
object[] param = new object[pi.Length];
foreach(ParameterInfo p in pi)
{
    if(p.ParameterType == typeof(string))
        param[p.Position] = "test";
}
object o = System.Activator.CreateInstance(typeof(SomeClass),param);
获取到了类的实例之后,然后获取类中的方法,具体操作已经在上面讲过,例子如下:

MethodInfo mi = typeof(SomeClass).GetMethod("SomeMethod");

现在,拥有了类的实例,也找到了类中的方法,接下来就可以使用MethodInfo.Invoke调用你的目标方法了,需要传递包含该方法的对象的实例和该方法需要的一组参数的值,比如:mi.Invoke(o,null);


第二部分:

这一部分的内容是反射的高级功能:Emit,即反射发出,它具有在运行时动态的产生代码的功效。它允许你从零开始,动态的构建程序集和类型的所有框架类的根。在需要时动态的产生代码。

注意:反射发出(reflection emit)并不能产生源代码。换句话说,你在这里的努力并不能创建C#代码。相反,反射发出(reflection emit)类会创建MSIL op代码。
使用反射发出(reflection emit)要遵循的过程

1. 创建一个新的程序集(程序集是动态的存在于内存中或把它们持久化到磁盘上)。

2. 在程序集内部,创建一个模块(module)。

3. 在模块内部,创建一个类型。

4.  给类型添加属性和方法。

5.  产生属性和方法内部的代码

第一步:构建程序集

在实际的操作中,第一步要遵循如下几个过程:

a)   创建一个AssemblyName(用于唯一标识和命名程序集),比如:AssemblyName name = new AssemblyName();name.Name = "MyAssembly";

b)   获取当前应用程序域的一个引用(使用应用程序域提供的方法,返回AssemblyBuilder对象),比如:AppDomain ad = System.Threading.Thread.GetDomain();

c)    通过调用AppDomain.DefineDynamicAssembly产生一个AssemblyBuilder对象实例,比如:AssemblyBuilder builder= ad.DefineDynamicAssembly(name,AssemblyBuilderAccess.Run);。其中AssemblyBuilderAccess枚举值表明,是想把程序集写入磁盘,保存到内存,还是两者都有。该方法中似要把程序集保存在内存里。

在上述的几个步骤中,AssemblyBuilder类是整个反射发出的工作支架,它提供了从零开始构造一个新的程序集的主要机制。
第二步:定义一个模块(module)

在这个步骤中,要在上个步骤中创建的AssemblyBuilder类实例中创建ModuleBuilder类实例。ModuleBuilder用于在一个程序集中创建一个模块。调用AssemblyBuilder对象上的DefineDynamicModule方法将会返回一个ModuleBuilder对象实例。在该方法中必须给这个模块命名(在这里,名字仅仅是个字符串)。比如:

ModuleBuilder mb =  builder.DefineDynamicModule("MyModule");

第三步:创建一个类型

在这个步骤中,需要使用一个方法(DefineType)从ModuleBuilder类实例中得到一个TypeBuilder对象的实例。

TypeBuilder theClass = mb.DefineType("MathOps",TypeAttributes.Public & TypeAttributes.Class);
该方法中使用TypeAttributes枚举指定了该类型的可见度为公共的。

第四步:添加一个方法

在这个步骤中,可以给上一步骤创建出来的类的实例中创建方法。

使用MethodBuilder类可以为你指定的类型定义方法。你可以在之前创建的类型对象上(theClass)调用DefineMethod获取一个MethodBuilder实例的引用。DefineMethod携带四个参数:方法的名称,方法可能的属性(如:public,private等等),方法的参数以及方法的返回值。如果没有参数和返回值,可以设置参数和返回值为void。

为了定义返回值的类型,创建一个包含返回类型值的类型对象(一个System.Int32类型的值):Type ret = typeof(System.Int32);

使用类型值数组定义方法的参数,这两个参数也是Int32的类型值Type[] param = new Type[2]; param[0= typeof(System.Int32); param[1= typeof(System.Int32);

有了这些值,你现在就可以调用DefineMethod方法了:MethodBuilder methodBuilder = theClass.DefineMethod("ReturnSum",MethodAttributes.Public,ret,param);

第五步:产生代码

在这个步骤中,开始添加方法的内部代码了。这是使用反射发出(reflection emit)产生代码的过程中真正核心的部分。

有一点是需要注意的,反射发出(reflection emit)的类不能产生源代码。换句话说,这里的结果并不会产生C#代码,而是产生MSIL op 代码。MSIL(微软中间语言)是一种接近于汇编程序的中间代码语言。当.NET JIT 编译器产生本地二进制代码的时候,就需要编译MSIL。Op代码是低级的,类似于汇编程序的操作指令。

考虑方法ReturnSum的如下实现:
public int ReturnSum(int val1,int val2)
{
    return val1 + val2;
}

如果你想“发出”这一段代码,你首先需要知道如何仅使用MSIL op代码编写这个方法。值得高兴的是,这里有一个快速,简单的办法可以做到。你可以简单的编译一下这段代码,然后使用.NET框架里的实用工具ildasm.exe查看程序集的结果。以下MSIL版本的代码是编译上面的方法产生的:
.method public hidebysig instance int32  ReturnSum(int32 val1, int32 val2) cil managed
{
.maxstack  2
.locals init ([
0] int32 CS$00000003$00000000)
IL_0000:  ldarg.
1
IL_0001:  ldarg.
2
IL_0002:  add
IL_0003:  stloc.
0
IL_0004:  br.s       IL_0006
IL_0006:  ldloc.
0
IL_0007:  ret
}

为了产生这段代码,你需要使用ILGenerator类。你可以调用MethodBuilder.GetILGenerator()方法获取对应方法上的ILGenerator类的一个实例:ILGenerator gen = methodBuilder.GetILGenerator();

使用gen对象,你可以把op指令注入到你的方法里:

gen.Emit(OpCodes.Ldarg_1);
gen.Emit(OpCodes.Ldarg_2);
gen.Emit(OpCodes.Add);
gen.Emit(OpCodes.Stloc_0);
gen.Emit(OpCodes.Br_S);
gen.Emit(OpCodes.Ldloc_0);
gen.Emit(OpCodes.Ret);

到此,你已经创建了方法,类,模块和程序集。为了得到这个类的一个引用,你可以调用CreateType,类似于下面的代码:theClass.CreateType();

Reflection.Emit的命名空间和类:

Namespace.Class

System.Reflection.Emit.AssemblyBuilder

主要用途

定义动态的.NET程序集:一种自我描述的 .NET内建块.动态程序集是通过反射发出特意产生的. 该类继承于System.Reflection.Assembly.

范例

Dim ab As AssemblyBuilderDim ad As AppDomainad = Thread.GetDomain()ab = ad.DefineDynamicAssembly(name, AssemblyBuilderAccess.Run)

Namespace.Class

System.Reflection.Emit.ConstructorBuilder

主要用途

用于创建和声明一个动态类的构造器.它囊括了有关构造器的所有信息,包括:名称,方法签名和主体代码.仅仅在你需要创建一个带参数的构造器或者需要覆盖父类构造器的默认行为的时候.

范例

Dim ourClass As TypeBuilder = [module].DefineType("ourClass", _TypeAttributes.Public)Dim ctorArgs As Type() = {GetType(String)}Dim ctor As ConstructorBuilder = _ourClass.DefineConstructor(MethodAttributes.Public, _CallingConventions.Standard, constructorArgs)

Namespace.Class

System.Reflection.Emit.CustomAttributeBuilder

主要用途

用于创建动态类的自定义特性.

Namespace.Class

System.Reflection.Emit.EnumBuilder

主要用途

定义和声明枚举.

Namespace.Class

System.Reflection.Emit.EventBuilder

主要用途

为动态类创建事件.

Namespace.Class

System.Reflection.Emit.FieldBuilder

主要用途

为动态类创建字段.

Namespace.Class

System.Reflection.Emit.ILGenerator

主要用途

用于产生MSIL代码.

范例

Dim gen As ILGenerator = someMethod.GetILGenerator()gen.Emit(OpCodes.Ldarg_0)gen.Emit(OpCodes.Ret)

Namespace.Class

System.Reflection.Emit.LocalBuilder

主要用途

创建方法或构造器的局部变量.

Namespace.Class

System.Reflection.Emit.MethodBuilder

主要用途

用于创建和声明动态类的方法.

Namespace.Class

System.Reflection.Emit.MethodRental

主要用途

一个很实用的类,用于从别的类中交换一个方法到动态创建的类中。当你需要快速重建一个已经在其它地方存在的方法时,就显得非常有用。

Namespace.Class

System.Reflection.Emit.ParameterBuilder

主要用途

为方法的签名创建参数.

Namespace.Class

System.Reflection.Emit.PropertyBuilder

主要用途

为动态的类型创建属性.

把反射发出和动态调用结合起来

现在你已经知道,如何使用反射类“发出”一个动态的程序集,那么让我们把反射发出和动态调用的内容(在第一部分讲到的)结合起来。

举个例子,在运行时何时使用Reflection和Reflection.Emit胜过代码或脚本赋值呢?这是有可能的,例如,显示一个带有输入框的窗体,要求用户输入一个公式,然后在运行时通过编译后的代码,求这个公式的值。


另外一种使用Reflection.Emit的时候,是为了使性能达到最优化。针对某一个问题,编码的解决方案,有时候故意的趋向于通用的解决方案。从设计的角度出发,这通常都是一件好事情,因为这会使你的系统更具有灵活性。例如,如果你想计算一些数字的和,在你设计的时候不必关心有多少个数字需要求和,因此你需要调用一个循环来解决这样的问题。如果你重写ReturnSum方法,让它接收一个Integer型的数组,你就需要在这个数组的成员之间循环,把每一个加到计数器上,然后返回所有数字的求和值。这是一个非常好的,通用的解决方法,因为它不必关心包含在数组中的值。

public int ReturnSum(int[] values)
{
        
int retVal;
        
for(int i=0;i<values.Length;i++)
        
{
            retVal 
+= values[i];
        }

        
return retVal;
}

另一方面,如果你硬编码数组的界限,那么你就可以通过编写一个长的数字操作语句来求和,这样的方式将会使代码达到更优化的状态。对于少量的值甚至几百个值而言,这两种编写方式带来的性能上的差距是可以忽略的。但是,如果你正在处理数千或者数百万的值,硬编码的方式将会非常非常的快。事实上,你可以把这个方法编写得更快,直接把数组中的值取出来相加,同时,把不影响结果的零值去掉。

public int ReturnSum()
{
    
return 9 + 32 + 8 + 1 + 2 + 2 + 90;
}


当然,这里的问题是,你编写的代码是不通用的和没有灵活性的。


因此,如何能够同时得到这两者的优点呢?答案是:使用Reflection.Emit。

通过把Reflection.Emit的功能(接收数组的上限和数组的值,然后产生数字直接相加的代码)和Reflection的功能(定位,加载并运行发出的程序集)融合在一起,你将能够打造出优雅的,具有独创性的性能解决方案,从而很好的避免了脆弱的代码。在这个简单的例子里,你可以写一个循环语句,产生你需要的MSIL op代码。


考虑下面的控制台程序,它接收一个数组,并创建一个新的程序集,模块,类和ReturnSum方法,它(ReturnSum)将直接求和数组中的值,而不是使用循环。代码如下:

 using System;
 using System.Data;
 using System.Reflection;
 using System.Reflection.Emit;
namespace ConsoleApplicationReflection
{
     class MathClassBuilder
     {
         /// <summary>
        /// 应用程序的主入口点。
        /// </summary>

        [STAThread]
        static void Main(string[] args)
        {
            try
            {
                Console.WriteLine("Enter values:");
                string numbers = Console.ReadLine();
                string[] values = numbers.Split(',');

                Type MathOpsClass = CreateType("OurAssembly","OurModule""MathOps""ReturnSum", values);

                object  MathOpsInst = Activator.CreateInstance(MathOpsClass);

                object obj = MathOpsClass.InvokeMember("ReturnSum",BindingFlags.InvokeMethod,null,MathOpsInst,null);

                Console.WriteLine("Sum: {0}",obj.ToString());
            }

            catch(Exception ex)
            {
                Console.WriteLine("An error occured: {0}",ex.Message);
            }


            Console.ReadLine();
        }
        
        public static Type CreateType(string assemblyName,string moduleName,string className,string methodName,string[] values)
        {
            try
            {
                AssemblyName name = new AssemblyName();
                name.Name = assemblyName;

                AppDomain domain = System.Threading.Thread.GetDomain();

                AssemblyBuilder assBuilder = domain.DefineDynamicAssembly(name,AssemblyBuilderAccess.Run);

                ModuleBuilder mb = assBuilder.DefineDynamicModule(moduleName);

                TypeBuilder theClass = mb.DefineType(className,TypeAttributes.Public | TypeAttributes.Class);
            
                Type rtnType = typeof(int);
            
                MethodBuilder method = theClass.DefineMethod(methodName,MethodAttributes.Public,rtnType,null);

                ILGenerator gen = method.GetILGenerator();

                gen.Emit(OpCodes.Ldc_I4,0);

                for(int i=0;i<values.Length;i++)
                {
                    gen.Emit(OpCodes.Ldc_I4,int.Parse(values[i]));
                    gen.Emit(OpCodes.Add);
                }

                gen.Emit(OpCodes.Ret);
                return theClass.CreateType();
            }

            catch(Exception ex)
            {
                Console.WriteLine("An error occured: {0}",ex.Message);
                return null;
            }

        }
        
    }
}
 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值