.net 反射

说明:原文中的代码为vb.net,我本人比较熟悉c#,所以在翻译的过程中,把vb.net代码替换成了c#代码,所以译文中的代码和原文中的代码会稍有出入,但这并不会影响你阅读。

何谓反射?

反射就是在运行的时候发现对象的相关信息。根据这些信息可以动态的执行对象的方法以及获取对象的属性所储存的值。使用.NET Framework编写的代码是自动反射的,或者说是自我描述的。之所以可以反射,是通过编译后产生的元数据来做到的。因此,你可以在你的程序中使用反射来查找托管代码中的类型(包括类的名称,方法以及参数)和与其相关的信息这其中包括执行被发现的代码。你也可以在程序运行的时候使用反射来创建,编译和运行代码。

反射的使用场所

有关反射最明显的例子就是类型或对象浏览器。想起Visual Studio .NET对象浏览器了吧。该工具可以显示程序集暴露的类,类的方法以及方法的参数等等(例如图一所示)。在过去,对我们有用的这些信息当中的一部分是通过COM类型库获取的。在.NET里,这些有用的程序数据完全可以从程序集本身来获取。所有的.NET程序集都是自我描述的,也就是说,它们包含着(它们的)类型的元数据,你可以通过查找元数据来了解一个特定的对象。
.net 反射_第1张图片

图一:Visual Studio .NET 对象浏览器

 

环顾.NET Framework框架和其各种不同的工具,你会很快的发现.NET本身在大量的使用反射。编译器在使用反射,其它的命名空间如:Remoting也在使用等等。如果你也喜欢,那么你肯定想知道:在我的程序中该如何使用反射呢?

有一点是毋庸置疑的,反射并不可能解决与你日常操作相关的业务程序的问题。比如:你不会使用Reflection命名空间像使用System.IO或者System.Data命名空间一样。但是,一旦你对反射为何物有一个清晰的认识,不久的将来,你将会意识到在很多特定的场合下应该使用反射。事实上,下面的描述将是使用反射的很好的场所:

建立在反射基础上的API帮助参考,自定义的元数据类型浏览器,关注于某一对象的代码测试工具产品,建立一个表单,允许用户执行代码像使用Web Serviceasmx文件一样,集中处理代码中出现的异常,记日志,在你任何需要的时候报告,为了使用户可以自由地连接部件(类似于销售渠道)而延迟绑定工作流程序。

从这个短小的清单中,你已经可以看到,反射给开发者们提供了很大的空间。让我们以最简单的方式来学习反射的相关知识吧。

反射的基础知识

当使用反射时,你的代码将会与一个基本的设计思路相吻合。首先,你需要加载一个程序集的元数据,然后在其中寻找你感兴趣的类型,最后,你将显示所得到的信息或者直接执行一个找到的类型。让我们关注一下这些在代码里是如何实现的。

首先,你必须在一个已知的类型或程序集上创建类Reflection.Assembly的一个实例,在这里不是使用一个构造器而是使用类Assembly的静态的方法:Load来实现的。该方法存在着一系列的版本,对于我们这个例子来说,我们只需要关心如下两个方法:

l         public static Assembly Load(string);

l         public static Assembly LoadFrom(string);

这两个方法都返回类Assembly的一个实例。Assembly.Load需要一个字符窜类型的参数,该参数表示要加载的程序集的名称(在当前的应用程序域内)。举个例子,如果你想创建一个简单的窗体程序,然后列举已经加载的程序集,这些程序集当中就有“System.Drawing,这是在窗体引擎中用到的程序集的名称(也是命名空间),因此要得到该程序集的实例,你就需要编写如下的代码:

 

Assembly myAssembly  =  Assembly.Load( " System.Drawing " );

你可以添加你自己要加载的程序集。也就是说,你的代码里可以读取已加载程序集的反射信息,做到这一步,只需要用你自己的程序集的名字替换上面方法Load里的System.Drawing

Assembly.LoadFrom以类似的方式工作,但是这个方法需要传递一个包含.NET程序集路径和名字的字符窜类型的参数。LoadFrom给了你使用任意.NET程序集的更好的选择,不仅仅是加载当前应用程序域内的程序集。如下所示:

string  path  =   @" C:/WINNT/Microsoft.NET/Framework/v1.1.4322/System.Drawing.dll " ;
Assembly myAssembly 
=  Assembly.LoadFrom(path);

C#开发人员也可以使用关键字typeof获取程序集,该运算符通过传递一个类型参数来获取该类型的System.Type对象,返回的对象里提供了访问其所在的程序集的途径,如下所示:

 

Assembly otherAssembly  =   typeof (System.Data.DataRow).Assembly;

 

当然,.NET框架也提供了另外一个有用方法,GetType。该方法可以用在所有.NET对象上。有关其例子,如下:

DataTable dt  =   new  DataTable();
Assembly otherAssembly 
=  dt.GetType().Assembly;


到这里,你已经了解了很多创建Reflection.Assembly实例的方法,让我们考虑一下,如何深入到程序集内部,查找其包含的相关信息。为了做这件事,你将会大量的使用System.Type类,它被用来表示所有的.NET类型(类,枚举,数组等)。你将会使用Assembly的一个实例返回一个Type实例数组,它包含了给定的程序集中所有的类型。举例说明,假设你有如下的一个控制台程序:


在这个程序集里定义了两个类型:testClass类和testEnum枚举。为了能够访问这两个类型,你需要调用Assembly.GetTypes,它将会以数组的方式返回程序集中定义的所有类型。如下面的例子,把下面的代码添加到之前的Main方法里,

Assembly myAssembly  =  Assembly.Load( " Basics " );
Type[] types 
=  myAssembly.GetTypes();
foreach (Type type  in  types)
{
    
//do something
}


你现在已经通过myAssembly.GetTypes获取了程序集中所有类型的引用并且可以在这些获取的类型中进行迭代操作。在你的代码里,你已经可以使用这些类型对象了。你可以添加如下的代码,来输出类的名字:

假设你想创建一个特定对象的实例,其构造器是需要传递参数的。为此你需要把这些参数的值作为一个数组的形式传递给CreateInstance。每一个参数的值需要对应该参数的类型,并且数组中参数的值需要与构造器的签名的顺序相一致。假如,你准备创建有如下构造器的类型的一个实例:

 

public  SomeClass( string  someParam)
{}

你的首要任务是查找带参数的构造器,只要确定了构造器,你就可以使用类ConstructorInfo的方法GetParameters得到其参数。GetParameters会返回一个ParameterInfo对象数组,它将会帮助你确定参数的顺序,参数的名称以及参数的类型。随后你就可以建立你自己的参数值的数组,然后把它传递给CreateInstance方法。下面是一个简单的例子。

假设你拥有一个SomeClass类,它有一个带一个string类型的参数的构造器,让我们继续假设你有一个SomeClass类型的引用myType。最后,让我们假定你拥有SomeClass的构造器的一个引用(名称为ci),该引用是ConstructorInfo的一个实例。为了获取给定构造器(ci)的参数列表,你会像下面一样调用GetParameters方法:

 

ParameterInfo[] pi  =  ci.GetParameters();

然后你会创建一个你自己的数组,容量与GetParameters返回的数组的长度一致。

 

object [] param  =   new   object [pi.Length];


给(即param)数组中的每个参数付值:

foreach (ParameterInfo p  in  pi)
{
    
if(p.ParameterType == typeof(string))
        param[p.Position] 
= "test";
}


最后,调用CreateInstance方法并把你要创建的类型和该类型构造器参数的值传给它即可。

object  o  =  System.Activator.CreateInstance( typeof (SomeClass),param);

到此,你已经得到了你的对象(SomeClass)的一个实例(o)。接下来,让我们了解一下,如何调用该对象的方法。在之前,我们查询构造器的参数并把参数的值传给构造器,对于方法而言,这个处理过程是一样的。假设SomaClass类有一个SomeMethod方法,你想调用这个方法。为了保证例子足够简单,假设方法SomeMethod没有任何参数(参数的处理过程同上)。为了能够调用SomeMethod,你需要获取关于该方法的MethodInfo对象的一个引用。在这里你可以使用GetMethod或者GetMethods方法在你的类型上搜索。让我们使用GetMethod,并给其传递方法的名称。

 

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

你不仅拥有了SomeClass的一个实例,而且也拥有了你希望调用该对象方法的引用mi,因此你可以使用MethodInfo.Invoke调用你的目标方法了。你需要传递包含该方法的对象的实例和该方法需要的一组参数的值。如下所示:

 

mi.Invoke(o, null );


你现在已经成功地创建了某个对象的一个实例,找到了该对象的某个方法,并且成功调用了此方法,而这些在设计之初没有必要知道该对象。你可以很容易的沿着这个例子向外延伸,创建一个类似于测试工具的实用工具。假设你允许用户选择一个程序集,那么你就可以罗列出包含在该程序集里所有的类和方法。在运行的时候,用户可以选择某一个方法,而你可以使用有关类和方法的已发现的信息给用户呈现一个窗体用于测试该方法。该窗体允许用户输入特定方法的值,测试工具则自动调用该方法并返回结果。而测试工具所做的这些事情在设计的时候均不用了解这些程序集。


综合起来(一个简单的类型浏览器)


为了说明利用反射创建一个.NET类型浏览器是多么的简单,我们综合以上的知识创建了一个简单的程序,给其命名为Discover,它可以浏览存储在任何地方的.NET程序集(.DLL.EXE)。当你选择了一个程序集后,你可以点击Discover按钮,这个程序将会以树状的形式展现出该程序集的信息。如图二所示:

.net 反射_第2张图片

该程序的Discover按钮的单击事件代码如下:

 

 1 if ( this .txtPath.Text.Trim()  ==   string .Empty)
 2                  return ;
 3 try
 4 {
 5                Assembly discoveryAssembly = Assembly.LoadFrom(this.txtPath.Text);
 6                TreeNode root = new TreeNode();
 7                root.Text = discoveryAssembly.GetName().Name;
 8            
 9                TreeNode nodeDetail = new TreeNode("Details");
10                nodeDetail.Nodes.Add(discoveryAssembly.FullName);
11                nodeDetail.Nodes.Add(discoveryAssembly.Location);
12                TreeNode nodeType = new TreeNode("Types");
13            
14                TreeNode nodeCur;
15                TreeNode nodeNext;
16                MemberInfo[] mis;
17                Type[] types = discoveryAssembly.GetTypes();
18                foreach(Type t in types)
19                {
20                    nodeCur = new TreeNode(t.FullName + "-" + t.UnderlyingSystemType.ToString());
21                    nodeType.Nodes.Add(nodeCur);
22                    nodeNext = new TreeNode("All Members");
23                    nodeCur.Nodes.Add(nodeNext);
24                    mis = t.GetMembers();
25                    foreach(MemberInfo m in mis)
26                    {
27                        nodeCur = new TreeNode(m.Name + "-" + m.MemberType.ToString());
28                        nodeNext.Nodes.Add(nodeCur);
29                    
30                        if(m.MemberType == MemberTypes.Method)
31                        {
32                            ParameterInfo[] pis = ((MethodInfo)m).GetParameters();
33                            TreeNode nodeParam;
34                            if(pis.Length > 0)
35                            {
36                                nodeParam = new TreeNode("Parameters");
37                                nodeCur.Nodes.Add(nodeParam);
38                                foreach(ParameterInfo p in pis)
39                                {
40                                    nodeParam.Nodes.Add(p.Name + "-" + p.ParameterType.ToString());
41                                }

42                            }

43                        
44                        }

45
46                    }

47                }

48                root.Nodes.Add(nodeDetail);
49                root.Nodes.Add(nodeType);
50                this.mytreeView.Nodes.Add(root);
51}

52 catch (Exception ex)
53 {
54    MessageBox.Show("出错了:"+ex.Message);
55}

56

(这些代码与作者原来的代码稍微有点不同的地方,但并不会影响该程序的功能。你可以在此基础上进行修改,创建你自己的类型浏览器。)


总结


希望这篇文章的内容可以帮助你把反射作为一个新的工具添加到你的程序中。该新工具可能并不能解决你在编写代码的过程中遇到的所有问题,但是如果用它做它所擅长的事情,它将会提供给你更多的选择空间。

反射系列的第二部分,我们将会阐述在运行时使用System.Reflection.Emit命名空间的类产生代码。

foreach (Type type  in  types)
{
    
if(type.IsClass)
        Console.WriteLine(type.Name);
}


到目前,你已经成功的加载了某个程序集,在其包含的类型中进行迭代,并且显示了程序集中所有类的名字。这里已经展示了使用反射跟程序集中的元数据打交道所用到的最基本的思路。该文章剩下来的部分将会深层次的向你展示反射中的类如何做更多的事情。

反射的命名空间和类

.NET框架里有两个命名空间与反射相关,System.ReflectionSystem.Reflection.EmitSystem.Reflection命名空间中包含着与类型的发现和执行相关的对象,而System.Reflection.Emit命名空间中则包含着在运行时动态的产生代码的相关对象(有关这一部分内容会在另一篇文章中重点描述)。


发现反射:搜索和过滤

在该文章的前面,你学会了如何加载一个程序集和获取对其中类型的访问。事实上,对于给定的.NET程序集,均可以使用Assembly.GetTypes获取其中所有的类型。该方法将会返回存在于给定的程序集中的全局和公共的类型(依赖于你的.NET安全模式或上下文)。举个例子,下面的代码在其自己的基础上创建了一个Assembly实例并且在其包含的类型中迭代。

using  System;
using  System.Reflection;
namespace  Basics
{
    
public class testClass
    
{
        
public static void Main()
        
{
            Assembly myAssembly 
= Assembly.Load("Basics");
            Type[] types 
= myAssembly.GetTypes();
            
foreach(Type type in types)
            
{
                Console.WriteLine(
"Type:{0}",type.Name);
            }

            Console.ReadLine();
        }

    }

}


对于给定的程序集,循环搜索其暴露的所有公共类型,看上去是个不错的主意。但是如果你的程序集很大,或者你只是想关注于特定的类型比如:构造器和属性,那么又该怎么办呢?如果你曾经关注过.NET框架的类库,你就会知道一个程序集中可能包含着非常多的类型。你可能并不需要知道每一个类型,你可能更喜欢通过搜索或者过滤来找到特定的类型。值得高兴的是,System.Type里提供了一系列的方法用于访问,搜索,过滤给定的程序集中特定的类型。

直接访问

直接访问一个给定的类型就说明,你已经知道并开始查找此种类型。可能你的程序只查找用户感兴趣的那些类型,或者可能从开始你就知道应该查找哪些特定的类型。在任何一种情况下,System.Type类已经提供了一系列可以直接访问特定类型的方法。类似于GetConstructorGetMethodGetPropertyGetEvent的方法允许你锁定特定的类型。示例如下,假如你有如下的类:

 

public   class  SomeClass
{
        
public SomeClass()
        
{}
        
public SomeClass(int someValue)
        
{}
        
public SomeClass(int someValue,int someOtherValue)
        
{}
        
public void SomeMethod()
        
{}
}


这个类有三个空的构造器和一个空方法。现在假设你想访问只有一个参数的那个构造器,为此你就需要使用Type类的GetConstructor方法,该方法允许你给它传递一个类型为Type的对象数组,该数组将被用于匹配构造器声明的参数。当执行的时候,此方法将找到这样一个构造器,该构造器的签名与定义参数中的数组相匹配。GetConstructor方法将返回一个ConstructorInfo对象供你使用(可能你会调用该构造器创建一个实例)。举例说明,你首先会创建类似于下下面的参数数组。

 

Type[] ts  =  { typeof (Int32)};

最后,你将会在你的Type对象上调用GetConstructor方法,如下所示:

 

ConstructorInfo ci  =   typeof (SomeClass).GetConstructor(ts);

同样,Type.GetMethod方法提供了在给定的对象上直接访问方法的途径。这个方法将返回一个MethodInfo实例供你使用。最简单的版本可以把给定的方法的名字当作参数传递给GetMethod,如下所示:

 

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

你可能会有疑问,如果我在一个类里有两个方法,它们具有相同的名字不同的签名,那会怎样呢?很好,在之前的例子里,你将会得到一个不明确匹配的异常(System.Reflection.AmbiguousMatchException异常错误),不过,这里还有很多直接访问方法的版本,这些将会使你更准确地得到特定的类型。对于GetConstructor方法,除了以对象数组作为参数的方式来筛选相应的构造器以外,还可以依据指定使用的一套规则等筛选你需要的构造器。你可以把这一套模式应用到直接访问你的类型中特定的属性或事件中。


过滤


System.Type
类也提供了一些方法,用于把包含在一个类里或者其它的类型里的特定的类型过滤到一个集合中。如GetConstructors方法,GetMethods方法,GetProperties方法和GetEvents方法均允许你以数组的方式返回所有给定的类型或者通过使用过滤条件只返回特定的类型集合。

一个典型的过滤器包括设置好的BindingFlagsBindingFlags表示过滤的条件,你可以使用枚举类BindingFlags的值表示一些如:public或者non-public类型。你也可以用这些标志来表示静态的成员,甚至你可以组合这些BindingFlags来缩小你的查找范围。假设你有如下一个简单的类。

 

public   class  OtherClass
{
        
public void OtherMethod()
        
{}
        
public static void OtherStaticMethod()
        
{}
        
public static void AnotherStaticMethod()
        
{}
}


在这里,你会看到,它有三个公共的方法,其中的两个为静态的方法。假设你想通过反射找出类OtherClass里的所有公共的静态方法,你将会调用GetMethods方法,并为其传递枚举

BindingFlags的值类似于bindingAttr这样的参数。如下的代码将会返回OtherClass类中公共静态的方法集合。

   

MethodInfo[] mis  =   typeof (OtherClass).GetMethods(BindingFlags.Public  |  BindingFlags.Static);

 

你可以使用相同的技术用于返回私有的类型(你需要有相应的权限)。为此,你可以使用BindingFlags的枚举值BindingFlags.NonPublic。这个也可以与BindingFlags.Instance一起使用,用于返回所有的私有的实例成员。这一套方式也可以被用于返回构造器,属性和事件。


搜索


正如你猜测的那样,搜索与过滤非常的类似,它们真正的不同之处在于搜索是通过System.Type的一个抽象的方法FindMembers来做到的。与其调用一个过滤器比如:GetEvents,倒不如你使用FindMembers,然后给它传递一个MemberType.Events的值作为memberType的参数,这样会让你拥有更多的灵活性。如果你需要一个自定义的过滤器,它并不包含某一明确的类型,你就可以使用FindMembers的参数来满足不同的搜索需要。接下来是个例子。

需要说明的是,下面的类定义了三个字段,二个私有的和一个共有的。


 

public   class  AnotherClass
{
        
private int myPrvField1 = 15;
        
private string myPrvField2 = "Some private field";
        
public decimal myPubField1 = 1.03m;
}


假设你需要使用Type类的方法FindMembers来获取AnotherClass实例上的私有字段,并且显示它们的值。你将会通过设置FindMembers的参数memberType的值为MemberType.Field,同时你还要设置BindingFlags的值,这样FindMembers将会返回与搜索条件匹配的MemberInfo对象数组。下面的代码片断展示了这样的例子:

FieldInfo fi;
AnotherClass ac 
=   new  AnotherClass();
MemberInfo[] memInfo 
=  ac.GetType().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对象,这样你就可以查找它们的值了。


自定义搜索


尽管全部使用上述的查找办法,你可能会发现,依然需要自定义一个搜索来满足你的要求。自定义搜索包含着之前的例子中用到的方法FindMembers。你可能已经注意到之前的例子中我们设置FindMembers的两个参数(filterfilterCriteria)为null值。filter参数传递一个MemberFilter委托对象的实例,通过定义一个委托,来自定义一个逻辑搜索。这个委托仅接收一个MemberInfo对象,它代表了满足其它搜索条件的类成员。

filterCriteria参数可以是任意的.NET对象。你可以使用这个参数定义你自己的搜索条件,它可以简单到一个string变量,或者包含一个自定义类的对象。接下来,我们举一个例子进行说明。

假如你现在有一个类SomeClass,它定义了三个属性,分别是:Name,IDType。现在假设你需要一个过滤器(filter),它仅仅搜索类中的NameID属性。我们已经提供了一个非常简单的例子,你当然可以返回所有的属性,在它们之间循环,过滤掉那些名字不是NameID的属性。让我们来看一下,这个控制台程序是如何实现的。

 

 1 using  System;
 2 using  System.Reflection;
 3 namespace  Basics2
 4 {
 5    /// 
 6    /// 被反射的类
 7    /// 

 8    public class SomeClass
 9    {
10        private int m_id;
11        public int ID
12        {
13            get{return this.m_id;}
14            set{this.m_id = value;}
15        }

16        private string m_name;
17        public string Name
18        {
19            get{return this.m_name;}
20            set{this.m_name = value;}
21        }

22        private int m_type;
23        public int Type
24        {
25            get{return this.m_type;}
26            set{this.m_type = value;}
27        }

28    }

29    /// 
30    /// 自定义的过滤对象类
31    /// 

32    public class filterObject
33    {
34        public string criterion1 = "Name";
35        public string criterion2 = "ID";
36    }

37    public class Basics
38    {
39        /// 
40        /// 自定义的搜索条件,回调的方法
41        /// 

42        /// 
43        /// 
44        /// 

45        public static bool MySearchDelegate(MemberInfo memberInfo,object filterCriteria)
46        {
47            if(memberInfo.Name == ((filterObject)filterCriteria).criterion1 || memberInfo.Name == ((filterObject)filterCriteria).criterion2)
48                return true;
49            return false;
50        }

51
52        public static void Main()
53        {
54            PropertyInfo pi;
55            //绑定自定义的搜索条件
56            MemberFilter mf = new MemberFilter(MySearchDelegate);
57            SomeClass sc = new SomeClass();
58            //使用FindMembers返回指定的属性
59            MemberInfo[] memInfo = sc.GetType().FindMembers(MemberTypes.Property,BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.Instance,mf,new filterObject());
60            foreach(MemberInfo m in memInfo)
61            {
62                pi = m as PropertyInfo;
63                Console.WriteLine(pi.Name);
64            }

65            Console.ReadLine();
66        }

67    }

68}

69

这个程序定义了一个委托(与委托MemberFilter具有相同签名的方法)MySearchDelegate,用于定制搜索条件。创建了一个类filterObject,其包含两个字段,辅助我们自定义搜索条件。程序中调用了FindMembers,指出我们需要所有的属性类型。当一个属性类型被发现时,程序将激发MySearchDelegate并且传给它一个filterCriteria实例对象,这个委托将判断成员的名称是否满足自定义的搜索条件来返回True还是False


执行发现的代码


在运行的时候能够发现类型的相关信息是很棒的,但是在得到反射的信息以后,如果可以在这些类型上执行一些操作则更具有魅力。这一点,我们已经可以做到。利用反射,可以让你在设计时编写的代码不用关心特定的类或者程序集。也就是说,你编写的这一段代码可以创建一个被发现的(通过反射程序集)程序集内的类的实例,找到类中的某个方法,得到方法的参数,并且可以执行这个方法。这就是后期绑定的结果:在运行的时候定位并且执行一个类型(创建该类型的实例或者执行其方法,而此类型在你设计之初并不知晓)。事实上,如果你在VB中使用后期绑定,那么编译器将会隐式的使用反射。让我们来看一下,如何显示的处理这种情况。

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

l         加载程序集

l         找到你希望使用的类型或者类

l         创建该类型(或者类)的一个实例

l         找到你希望执行的该类型的某个方法

l         得到该方法的参数

l         调用该对象上的方法并且传递给它恰当的参数

你已经知道如何加载一个程序集和在该程序集中搜索类型。一旦你找到你要找的类型,你就可以使用System.Activator创建此类型的一个实例。你将会使用Activator类的方法CreateInstance众多版本中的一个。CreateInstance允许你指定你想要创建的对象,并且可选择的参数会应用到该对象的构造器上。这里有一个简单的例子,在这里该对象的默认的构造器不用传递参数:

 

// SomeClass为一个类
object  obj  =  System.Activator.CreateInstance( typeof (SomeClass));


c#反射/映射 学习摘录

最近想研究一下反射,先上网找了找资料,几乎大部分都是照抄MSDN的内容,生涩难懂,几乎没说,又找了找,发现一些强人的实例解析,才稍微有了点门道,个人感觉,反射其实就是为了能够在程序运行期间动态的加载一个外部的DLL集合,然后通过某种办法找到这个DLL集合中的某个空间下的某个类的某个成员(通过反射可以访问该类所包含的所有成员,不论成员是公有还是私有),看看网上N人写的实例:

1.运用反射调用其它程序集中的方法:  
  假设另一个工程中的所有类都编译到一个dll文件中了,在这很多的类当中,有一个类叫StringUtil,名称空间在HSMP.CommonBasic.Common下  
  该类中有一个方法:  
  public   double   GetSum(double   x,double   y)  
  {  
  return   x+y;  
  }  
  编译后dll文件的存放路径是:D:/Test/HSMP.CommonBasic.dll  
  现在的问题是,如何通过程序调用该dll文件中的GetSum方法  
  大概有以下几步:  
  using   System.Reflection;  
  A.  
  //这里要用LoadFrom,只有在本工程里添加了该dll的引用后才可以使用Load  
  Assembly   objAss   =   Assembly.LoadFrom(@"D:/Test/HSMP.CommonBasic.dll");  
  //HSMP.CommonBasic.Common.StringUtil类的全路径  
  Type   t=objAss.GetType("HSMP.CommonBasic.Common.StringUtil");    
  //动态生成类StringUtil的实例  
  object   obj=System.Activator.CreateInstance(t);  
  //参数信息,GetSum需要两个int参数,如果方法没有参数,就声明一个长度为0的数组  
  System.Type[]   paramTypes   =   new   System.Type[2];  
  paramTypes[0]   =   System.Type.GetType("System.Int32");  
  paramTypes[1]   =   System.Type.GetType("System.Int32");  
  //找到对应的方法  
  MethodInfo   p   =   t.GetMethod("SayHello",   paramTypes)  
  //参数值,如果所调用的方法没有参数,不用写这些  
  Object[]   parameters   =   new   Object[2];  
  parameters[0]   =   3;  
  parameters[1]   =   4;  
  object   objRetval   =   p.Invoke(obj,   parameters);   //如果没有参数,写null即可。  


2.动态加载, 更改, 增加...某个程序集
下面例子中, ChangeValue类的myValue本是私有字段,
一般情况下在类外部是不能改它的值的, 但利用反射就能改了
-----------------------------------------------------
using System;
using System.Reflection;

class ChangeValue
{
private string myValue;
public ChangeValue(string str)
{
myValue = str;
}
public void WriteLine()
{
Console.WriteLine("MyValue is: " + myValue);
}
}
class Test
{
public static void Main(string[] args)
{
ChangeValue cv = new ChangeValue("old value");
cv.WriteLine();

Type t = cv.GetType();
FieldInfo field = t.GetField("myValue",
BindingFlags.NonPublic |BindingFlags.Instance);
field.SetValue(cv, "new value");

cv.WriteLine();
}
}
----------------------------------------------------
再打个比方, 你要写一个播放器, 要支持如mp3, wmv, avi...等格式,
你还希望用户能自己安装一个新的格式, 也就是我们常说的插件.
在实现这些, 可能你要将每种格式都写成单个的解码程序集, 如
mp3.dll, wmv.dll, avi.dll....
这样当播放时, 根据扩展名去动态调用相应的解码程序集, 那么这时你就得
用反射去动态加载这些dll了.如: Assembly.LoadFile ("...avi.dll")
然后通过反射可以用avi.dll里面定义的类了
 

在我们之前的文章, 反射的第一部分:发现和执行里,我们已经介绍了System.Reflection命名空间及其包含的类,开发人员使用这些类可以查看程序集的元数据,并且可以在运行时查找和发现类型,甚至可以调用发现的代码。在这篇文章中,我们将探讨反射的高级功能:Emit,它具有在运行时动态的产生代码的功效。

回顾反射

首先,让我们快速的回顾一下,什么是反射以及反射可以被用来做什么。从第一部分内容中,你已经知道,反射是在运行时发现对象的相关信息,并且执行这些对象(创建对象实例,执行对象上的方法)。这个功能是由.NET的System.Reflection命名空间的类所提供的。这些被用于类型发现和动态调用的类包括:Assembly,Module,ConstructorInfo,MethodInfo以及其它。简单的说,它们不仅允许你浏览一个程序集暴露的类、方法、属性和字段,而且还允许你创建一个类型的实例以及执行这些类型上的方法(调用成员)。这些特性对于在运行时对象发现,已经很了不起了,但.NET的反射机制并没有到此结束。反射还允许你在运行时构建一个程序集,并且可以创建全新的类型。这就是反射发出(reflection emit)。

何谓反射发出(
Reflection Emit

System..Reflection.Emit命名空间嵌套在System.Reflection的下面,它是,允许你从零开始,动态的构建程序集和类型的所有框架类的根。在需要时动态的产生代码,类似这样的操作,虽然很少的开发人员会需要,但这对于.NET框架是一种凭据,证明有这样的工具可以解决有这样需求的业务问题。

注意:反射发出(
reflection emit )并不能产生源代码。换句话说,你在这里的努力并不能创建 VB.Net 或者 C# 代码。相反,反射发出( reflection emit )类会创建 MSIL op 代码。

作为例子,使用反射发出(reflection emit)可能会是这样子的:
1.         创建一个新的程序集(程序集是动态的存在于内存中或把它们持久化到磁盘上)。
2.         在程序集内部,创建一个模块(module)。
3.         在模块内部,创建一个类型。
4.         给类型添加属性和方法。
5.         产生属性和方法内部的代码
确切得说,当你使用Reflection.Emit类产生代码时,以上描述的是你实际中要遵循的过程。

代码生成的过程

依照上面列出的步骤,让我们探讨一下构建一个程序集,必要的操作。为此,我们举个非常简单的例子。假设你想构建一个类MathOps,它有一个公共的方法(函数),这个方法接收两个Integer类型的参数,然后返回它们的相加后的值。

第一步:构建程序集

稍微扩充一下上面列出的步骤,在实际的操作中,第一步更像是如下所述:
a)         创建一个AssemblyName(用于唯一标识和命名程序集)。
b)        获取当前应用程序域的一个引用(使用应用程序域提供的方法,返回AssemblyBuilder对象)。
c)        通过调用AppDomain.DefineDynamicAssembly产生一个AssemblyBuilder对象实例。
为了开始程序集的构建过程,你首先需要创建一个AssemblyName实例,用于标识你的程序集。如下:

 

AssemblyName name  =  new  AssemblyName();
name.Name 
=  " MyAssembly " ;


接下来,你需要 System.AppDomain类的一个实例。你可以从当前运行的线程实例中获取。


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


这两个实例创建以后,你现在就可以定义一个 AssemblyBuilder变量,然后使用之前创建的AssemblyName和AppDomain的实例把它实例化。AssemblyBuilder类是整个反射发出的工作支架。它给你,从零开始构造一个新的程序集提供了主要的机制。除此之外,你还需要指定一个AssemblyBuilderAccess枚举值,它将表明,你是想把程序集写入磁盘,保存到内存,还是两者都有。在这个例子里,你想把程序集保存在内存里。

 

AssemblyBuilder builder;
builder 
=  ad.DefineDynamicAssembly(name,AssemblyBuilderAccess.Run);


第二步:定义一个模块(module)

在第二步里,你将使用ModuleBuilder类,在你之前创建的程序集(builder)里创建一个动态的模块。ModuleBuilder用于在一个程序集中创建一个模块。调用AssemblyBuilder对象上的DefineDynamicModule方法将会返回一个ModuleBuilder对象实例。跟程序集一样,你必须给这个模块命名(在这里,名字仅仅是个字符窜)。
 
 

 

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

 


 
第三步:创建一个类型

现在,你已经拥有了一个程序集,一个模块,你可以把你的类添加到这个程序集中。为了创建一个新的类型,你需要使用 TypeBuilder类。你将会使用一个方法(DefineType)从“父对象”(指mb)得到一个TypeBuilder对象的实例。
 

 

TypeBuilder theClass;
theClass 
=  mb.DefineType( " MathOps " ,TypeAttributes.Public  &  TypeAttributes.Class);

 

注意,你已经使用 TypeAttributes枚举指定了该类型的可见度为公共的。

第四步:添加一个方法

既然所需的类型已经创建好了,那么你就可以给它添加方法了。即将添加的方法命名为 ReturnSum,可见度为公共的。
使用 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;
methodBuilder 
=  theClass.DefineMethod( " ReturnSum " ,MethodAttributes.Public,ret,param);

 

第五步:产生代码

截止到现在,在第四步里,方法的框架已基本上搭建起来,你现在需要做的是添加方法的内部代码。这是使用反射发出( reflection emit)产生代码的过程中真正核心的部分。
有一点是需要注意的,反射发出( reflection emit)的类不能产生源代码。换句话说,这里的结果并不会产生Visual Basic.NET或者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
{
  
// 代码大小       8 (0x8)
  .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
}
  //  end of method Class1::ReturnSum


为了产生这段代码,你需要使用 ILGenerator类。你可以调用MethodBuilder.GetILGenerator()方法获取对应方法上的ILGenerator类的一个实例。换句话说,如下的代码将会获取你的方法ReturnMethod上的一个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命名空间包含一系列核心“构建”类,它们用于创建类型和与新类型相关的,如:各种特性,方法,字段,属性等等。Table 1描述了使用反射产生代码用到的主要的类。

Table 1 反射发出相关类的参考

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)将直接求和数组中的值,而不是使用循环。代码如下:

 

 1 using  System;
 2 using  System.Data;
 3 using  System.Reflection;
 4 using  System.Reflection.Emit;
 5 namespace  ConsoleApplicationReflection
 6 {
 7    class MathClassBuilder
 8    {
 9        /// 
10        /// 应用程序的主入口点。
11        /// 

12        [STAThread]
13        static void Main(string[] args)
14        {
15            try
16            {
17                Console.WriteLine("Enter values:");
18                string numbers = Console.ReadLine();
19                string[] values = numbers.Split(',');
20
21                Type MathOpsClass = CreateType("OurAssembly","OurModule""MathOps""ReturnSum", values);
22
23                object  MathOpsInst = Activator.CreateInstance(MathOpsClass);
24
25                object obj = MathOpsClass.InvokeMember("ReturnSum",BindingFlags.InvokeMethod,null,MathOpsInst,null);
26
27                Console.WriteLine("Sum: {0}",obj.ToString());
28            }

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

33
34
35            Console.ReadLine();
36        }

37        
38        public static Type CreateType(string assemblyName,string moduleName,string className,string methodName,string[] values)
39        {
40            try
41            {
42                AssemblyName name = new AssemblyName();
43                name.Name = assemblyName;
44
45                AppDomain domain = System.Threading.Thread.GetDomain();
46
47                AssemblyBuilder assBuilder = domain.DefineDynamicAssembly(name,AssemblyBuilderAccess.Run);
48
49                ModuleBuilder mb = assBuilder.DefineDynamicModule(moduleName);
50
51                TypeBuilder theClass = mb.DefineType(className,TypeAttributes.Public | TypeAttributes.Class);
52            
53                Type rtnType = typeof(int);
54            
55                MethodBuilder method = theClass.DefineMethod(methodName,MethodAttributes.Public,rtnType,null);
56
57                ILGenerator gen = method.GetILGenerator();
58
59                gen.Emit(OpCodes.Ldc_I4,0);
60
61                for(int i=0;i<values.Length;i++)
62                {
63                    gen.Emit(OpCodes.Ldc_I4,int.Parse(values[i]));
64                    gen.Emit(OpCodes.Add);
65                }

66                gen.Emit(OpCodes.Ret);
67
68                return theClass.CreateType();
69            }

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

75        }

76        
77    }

78}

你可能感兴趣的:(.net 反射)