(翻译)反射的第一部分:发现和执行
原文地址
作者:Mike Snell
翻译:today
说明:原文中的代码为vb.net,我本人比较熟悉c#,所以在翻译的过程中,把vb.net代码替换成了c#代码,所以译文中的代码和原文中的代码会稍有出入,但这并不会影响你阅读。
何谓反射?
反射就是在运行的时候发现对象的相关信息。根据这些信息可以动态的执行对象的方法以及获取对象的属性所储存的值。使用.NET Framework编写的代码是自动反射的,或者说是自我描述的。之所以可以反射,是通过编译后产生的元数据来做到的。因此,你可以在你的程序中使用反射来查找托管代码中的类型(包括类的名称,方法以及参数)和与其相关的信息这其中包括执行被发现的代码。你也可以在程序运行的时候使用反射来创建,编译和运行代码。
反射的使用场所
有关反射最明显的例子就是类型或对象浏览器。想起Visual Studio .NET对象浏览器了吧。该工具可以显示程序集暴露的类,类的方法以及方法的参数等等(例如图一所示)。在过去,对我们有用的这些信息当中的一部分是通过COM类型库获取的。在.NET里,这些有用的程序数据完全可以从程序集本身来获取。所有的.NET程序集都是自我描述的,也就是说,它们包含着(它们的)类型的元数据,你可以通过查找元数据来了解一个特定的对象。
图一:Visual Studio .NET 对象浏览器
环顾.NET Framework框架和其各种不同的工具,你会很快的发现.NET本身在大量的使用反射。编译器在使用反射,其它的命名空间如:Remoting也在使用等等。如果你也喜欢,那么你肯定想知道:在我的程序中该如何使用反射呢?
有一点是毋庸置疑的,反射并不可能解决与你日常操作相关的业务程序的问题。比如:你不会使用Reflection命名空间像使用System.IO或者System.Data命名空间一样。但是,一旦你对反射为何物有一个清晰的认识,不久的将来,你将会意识到在很多特定的场合下应该使用反射。事实上,下面的描述将是使用反射的很好的场所:
建立在反射基础上的API帮助参考,自定义的元数据类型浏览器,关注于某一对象的代码测试工具产品,建立一个表单,允许用户执行代码像使用Web Service的asmx文件一样,集中处理代码中出现的异常,记日志,在你任何需要的时候报告,为了使用户可以自由地连接部件(类似于销售渠道)而延迟绑定工作流程序。
从这个短小的清单中,你已经可以看到,反射给开发者们提供了很大的空间。让我们以最简单的方式来学习反射的相关知识吧。
反射的基础知识
当使用反射时,你的代码将会与一个基本的设计思路相吻合。首先,你需要加载一个程序集的元数据,然后在其中寻找你感兴趣的类型,最后,你将显示所得到的信息或者直接执行一个找到的类型。让我们关注一下这些在代码里是如何实现的。
首先,你必须在一个已知的类型或程序集上创建类Reflection.Assembly的一个实例,在这里不是使用一个构造器而是使用类Assembly的静态的方法:Load来实现的。该方法存在着一系列的版本,对于我们这个例子来说,我们只需要关心如下两个方法:
l public static Assembly Load(string);
l public static Assembly LoadFrom(string);
这两个方法都返回类Assembly的一个实例。Assembly.Load需要一个字符窜类型的参数,该参数表示要加载的程序集的名称(在当前的应用程序域内)。举个例子,如果你想创建一个简单的窗体程序,然后列举已经加载的程序集,这些程序集当中就有“System.Drawing”,这是在窗体引擎中用到的程序集的名称(也是命名空间),因此要得到该程序集的实例,你就需要编写如下的代码:
你可以添加你自己要加载的程序集。也就是说,你的代码里可以读取已加载程序集的反射信息,做到这一步,只需要用你自己的程序集的名字替换上面方法Load里的System.Drawing。
Assembly.LoadFrom以类似的方式工作,但是这个方法需要传递一个包含.NET程序集路径和名字的字符窜类型的参数。LoadFrom给了你使用任意.NET程序集的更好的选择,不仅仅是加载当前应用程序域内的程序集。如下所示:
Assembly myAssembly = Assembly.LoadFrom(path);
C#开发人员也可以使用关键字typeof获取程序集,该运算符通过传递一个类型参数来获取该类型的System.Type对象,返回的对象里提供了访问其所在的程序集的途径,如下所示:
当然,.NET框架也提供了另外一个有用方法,GetType。该方法可以用在所有.NET对象上。有关其例子,如下:
Assembly otherAssembly = dt.GetType().Assembly;
到这里,你已经了解了很多创建Reflection.Assembly实例的方法,让我们考虑一下,如何深入到程序集内部,查找其包含的相关信息。为了做这件事,你将会大量的使用System.Type类,它被用来表示所有的.NET类型(类,枚举,数组等)。你将会使用Assembly的一个实例返回一个Type实例数组,它包含了给定的程序集中所有的类型。举例说明,假设你有如下的一个控制台程序:
1using System;
2using System.Reflection;
3namespace Basics
4{
5 public enum testEnum
6 {
7 testValue = 1
8 }
9 public class testClass
10 {
11 public static void Main()
12 {
13
14 }
15 }
16}
17
在这个程序集里定义了两个类型:testClass类和testEnum枚举。为了能够访问这两个类型,你需要调用Assembly.GetTypes,它将会以数组的方式返回程序集中定义的所有类型。如下面的例子,把下面的代码添加到之前的Main方法里,
Type[] types = myAssembly.GetTypes();
foreach(Type type in types)
{
//do something
}
你现在已经通过myAssembly.GetTypes获取了程序集中所有类型的引用并且可以在这些获取的类型中进行迭代操作。在你的代码里,你已经可以使用这些类型对象了。你可以添加如下的代码,来输出类的名字:
{
if(type.IsClass)
Console.WriteLine(type.Name);
}
到目前,你已经成功的加载了某个程序集,在其包含的类型中进行迭代,并且显示了程序集中所有类的名字。这里已经展示了使用反射跟程序集中的元数据打交道所用到的最基本的思路。该文章剩下来的部分将会深层次的向你展示反射中的类如何做更多的事情。
反射的命名空间和类
.NET框架里有两个命名空间与反射相关,System.Reflection和System.Reflection.Emit。System.Reflection命名空间中包含着与类型的发现和执行相关的对象,而System.Reflection.Emit命名空间中则包含着在运行时动态的产生代码的相关对象(有关这一部分内容会在另一篇文章中重点描述)。
发现反射:搜索和过滤
在该文章的前面,你学会了如何加载一个程序集和获取对其中类型的访问。事实上,对于给定的.NET程序集,均可以使用Assembly.GetTypes获取其中所有的类型。该方法将会返回存在于给定的程序集中的全局和公共的类型(依赖于你的.NET安全模式或上下文)。举个例子,下面的代码在其自己的基础上创建了一个Assembly实例并且在其包含的类型中迭代。
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类已经提供了一系列可以直接访问特定类型的方法。类似于GetConstructor,GetMethod,GetProperty和GetEvent的方法允许你锁定特定的类型。示例如下,假如你有如下的类:
{
public SomeClass()
{}
public SomeClass(int someValue)
{}
public SomeClass(int someValue,int someOtherValue)
{}
public void SomeMethod()
{}
}
这个类有三个空的构造器和一个空方法。现在假设你想访问只有一个参数的那个构造器,为此你就需要使用Type类的GetConstructor方法,该方法允许你给它传递一个类型为Type的对象数组,该数组将被用于匹配构造器声明的参数。当执行的时候,此方法将找到这样一个构造器,该构造器的签名与定义参数中的数组相匹配。GetConstructor方法将返回一个ConstructorInfo对象供你使用(可能你会调用该构造器创建一个实例)。举例说明,你首先会创建类似于下下面的参数数组。
最后,你将会在你的Type对象上调用GetConstructor方法,如下所示:
同样,Type.GetMethod方法提供了在给定的对象上直接访问方法的途径。这个方法将返回一个MethodInfo实例供你使用。最简单的版本可以把给定的方法的名字当作参数传递给GetMethod,如下所示:
你可能会有疑问,如果我在一个类里有两个方法,它们具有相同的名字不同的签名,那会怎样呢?很好,在之前的例子里,你将会得到一个不明确匹配的异常(System.Reflection.AmbiguousMatchException异常错误),不过,这里还有很多直接访问方法的版本,这些将会使你更准确地得到特定的类型。对于GetConstructor方法,除了以对象数组作为参数的方式来筛选相应的构造器以外,还可以依据指定使用的一套规则等筛选你需要的构造器。你可以把这一套模式应用到直接访问你的类型中特定的属性或事件中。
过滤
System.Type类也提供了一些方法,用于把包含在一个类里或者其它的类型里的特定的类型过滤到一个集合中。如GetConstructors方法,GetMethods方法,GetProperties方法和GetEvents方法均允许你以数组的方式返回所有给定的类型或者通过使用过滤条件只返回特定的类型集合。
一个典型的过滤器包括设置好的BindingFlags,BindingFlags表示过滤的条件,你可以使用枚举类BindingFlags的值表示一些如:public或者non-public类型。你也可以用这些标志来表示静态的成员,甚至你可以组合这些BindingFlags来缩小你的查找范围。假设你有如下一个简单的类。
{
public void OtherMethod()
{}
public static void OtherStaticMethod()
{}
public static void AnotherStaticMethod()
{}
}
在这里,你会看到,它有三个公共的方法,其中的两个为静态的方法。假设你想通过反射找出类OtherClass里的所有公共的静态方法,你将会调用GetMethods方法,并为其传递枚举
类BindingFlags的值类似于bindingAttr这样的参数。如下的代码将会返回OtherClass类中公共静态的方法集合。
你可以使用相同的技术用于返回私有的类型(你需要有相应的权限)。为此,你可以使用BindingFlags的枚举值BindingFlags.NonPublic。这个也可以与BindingFlags.Instance一起使用,用于返回所有的私有的实例成员。这一套方式也可以被用于返回构造器,属性和事件。
搜索
正如你猜测的那样,搜索与过滤非常的类似,它们真正的不同之处在于搜索是通过System.Type的一个抽象的方法FindMembers来做到的。与其调用一个过滤器比如:GetEvents,倒不如你使用FindMembers,然后给它传递一个MemberType.Events的值作为memberType的参数,这样会让你拥有更多的灵活性。如果你需要一个自定义的过滤器,它并不包含某一明确的类型,你就可以使用FindMembers的参数来满足不同的搜索需要。接下来是个例子。
需要说明的是,下面的类定义了三个字段,二个私有的和一个共有的。
{
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对象数组。下面的代码片断展示了这样的例子:
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的两个参数(filter和filterCriteria)为null值。filter参数传递一个MemberFilter委托对象的实例,通过定义一个委托,来自定义一个逻辑搜索。这个委托仅接收一个MemberInfo对象,它代表了满足其它搜索条件的类成员。
filterCriteria参数可以是任意的.NET对象。你可以使用这个参数定义你自己的搜索条件,它可以简单到一个string变量,或者包含一个自定义类的对象。接下来,我们举一个例子进行说明。
假如你现在有一个类SomeClass,它定义了三个属性,分别是:Name,ID和Type。现在假设你需要一个过滤器(filter),它仅仅搜索类中的Name和ID属性。我们已经提供了一个非常简单的例子,你当然可以返回所有的属性,在它们之间循环,过滤掉那些名字不是Name和ID的属性。让我们来看一下,这个控制台程序是如何实现的。
2using System.Reflection;
3namespace Basics2
4{
5 /**//// <summary>
6 /// 被反射的类
7 /// </summary>
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 /**//// <summary>
30 /// 自定义的过滤对象类
31 /// </summary>
32 public class filterObject
33 {
34 public string criterion1 = "Name";
35 public string criterion2 = "ID";
36 }
37 public class Basics
38 {
39 /**//// <summary>
40 /// 自定义的搜索条件,回调的方法
41 /// </summary>
42 /// <param name="memberInfo"></param>
43 /// <param name="filterCriteria"></param>
44 /// <returns></returns>
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允许你指定你想要创建的对象,并且可选择的参数会应用到该对象的构造器上。这里有一个简单的例子,在这里该对象的默认的构造器不用传递参数:
object obj = System.Activator.CreateInstance(typeof(SomeClass));
假设你想创建一个特定对象的实例,其构造器是需要传递参数的。为此你需要把这些参数的值作为一个数组的形式传递给CreateInstance。每一个参数的值需要对应该参数的类型,并且数组中参数的值需要与构造器的签名的顺序相一致。假如,你准备创建有如下构造器的类型的一个实例:
{}
你的首要任务是查找带参数的构造器,只要确定了构造器,你就可以使用类ConstructorInfo的方法GetParameters得到其参数。GetParameters会返回一个ParameterInfo对象数组,它将会帮助你确定参数的顺序,参数的名称以及参数的类型。随后你就可以建立你自己的参数值的数组,然后把它传递给CreateInstance方法。下面是一个简单的例子。
假设你拥有一个SomeClass类,它有一个带一个string类型的参数的构造器,让我们继续假设你有一个SomeClass类型的引用myType。最后,让我们假定你拥有SomeClass的构造器的一个引用(名称为ci),该引用是ConstructorInfo的一个实例。为了获取给定构造器(ci)的参数列表,你会像下面一样调用GetParameters方法:
然后你会创建一个你自己的数组,容量与GetParameters返回的数组的长度一致。
给(即param)数组中的每个参数付值:
{
if(p.ParameterType == typeof(string))
param[p.Position] = "test";
}
最后,调用CreateInstance方法并把你要创建的类型和该类型构造器参数的值传给它即可。
到此,你已经得到了你的对象(SomeClass)的一个实例(o)。接下来,让我们了解一下,如何调用该对象的方法。在之前,我们查询构造器的参数并把参数的值传给构造器,对于方法而言,这个处理过程是一样的。假设SomaClass类有一个SomeMethod方法,你想调用这个方法。为了保证例子足够简单,假设方法SomeMethod没有任何参数(参数的处理过程同上)。为了能够调用SomeMethod,你需要获取关于该方法的MethodInfo对象的一个引用。在这里你可以使用GetMethod或者GetMethods方法在你的类型上搜索。让我们使用GetMethod,并给其传递方法的名称。
你不仅拥有了SomeClass的一个实例,而且也拥有了你希望调用该对象方法的引用mi,因此你可以使用MethodInfo.Invoke调用你的目标方法了。你需要传递包含该方法的对象的实例和该方法需要的一组参数的值。如下所示:
你现在已经成功地创建了某个对象的一个实例,找到了该对象的某个方法,并且成功调用了此方法,而这些在设计之初没有必要知道该对象。你可以很容易的沿着这个例子向外延伸,创建一个类似于测试工具的实用工具。假设你允许用户选择一个程序集,那么你就可以罗列出包含在该程序集里所有的类和方法。在运行的时候,用户可以选择某一个方法,而你可以使用有关类和方法的已发现的信息给用户呈现一个窗体用于测试该方法。该窗体允许用户输入特定方法的值,测试工具则自动调用该方法并返回结果。而测试工具所做的这些事情在设计的时候均不用了解这些程序集。
综合起来(一个简单的类型浏览器)
为了说明利用反射创建一个.NET类型浏览器是多么的简单,我们综合以上的知识创建了一个简单的程序,给其命名为Discover,它可以浏览存储在任何地方的.NET程序集(.DLL和.EXE)。当你选择了一个程序集后,你可以点击Discover按钮,这个程序将会以树状的形式展现出该程序集的信息。如图二所示:
该程序的Discover按钮的单击事件代码如下:
2 return;
3try
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}
52catch(Exception ex)
53{
54 MessageBox.Show("出错了:"+ex.Message);
55}
56
(这些代码与作者原来的代码稍微有点不同的地方,但并不会影响该程序的功能。你可以在此基础上进行修改,创建你自己的类型浏览器。)
总结
希望这篇文章的内容可以帮助你把反射作为一个新的工具添加到你的程序中。该新工具可能并不能解决你在编写代码的过程中遇到的所有问题,但是如果用它做它所擅长的事情,它将会提供给你更多的选择空间。
反射系列的第二部分,我们将会阐述在运行时使用System.Reflection.Emit命名空间的类产生代码。