net 反射机制

14 篇文章 0 订阅
13 篇文章 0 订阅

 

4月7日
.net 反射机制
提纲:
1、 什么是反射
2、 命名空间与装配件的关系
3、 运行期得到类型信息有什么用
4、 如何使用反射获取类型
5、 如何根据类型来动态创建对象
6、 如何获取方法以及动态调用方法
7、 动态创建委托

 

1、什么是反射
    Reflection,中文翻译为反射。
    这是.Net中获取运行时类型信息的方式,.Net的应用程序由几个部分:‘程序集(Assembly)’、‘模块(Module)’、‘类型(class)’组成,而反射提供一种编程的方式,让程序员可以在程序运行期获得这几个组成部分的相关信息,例如:

    Assembly类可以获得正在运行的装配件信息,也可以动态的加载装配件,以及在装配件中查找类型信息,并创建该类型的实例。
Type类可以获得对象的类型信息,此信息包含对象的所有要素:方法、构造器、属性等等,通过Type类可以得到这些要素的信息,并且调用之。
MethodInfo包含方法的信息,通过这个类可以得到方法的名称、参数、返回值等,并且可以调用之。
诸如此类,还有FieldInfo、EventInfo等等,这些类都包含在System.Reflection命名空间下。

2、命名空间与装配件的关系
    很多人对这个概念可能还是很不清晰,对于合格的.Net程序员,有必要对这点进行澄清。
    命名空间类似与Java的包,但又不完全等同,因为Java的包必须按照目录结构来放置,命名空间则不需要。

    装配件是.Net应用程序执行的最小单位,编译出来的.dll、.exe都是装配件。

    装配件和命名空间的关系不是一一对应,也不互相包含,一个装配件里面可以有多个命名空间,一个命名空间也可以在多个装配件中存在,这样说可能有点模糊,举个例子:
装配件A:
namespace N1
{
   public class AC1 {…}
   public class AC2 {…}
}
namespace N2
{
   public class AC3 {…}
   public class AC4{…}
}
装配件B:
namespace N1
{
   public class BC1 {…}
   public class BC2 {…}
}
namespace N2
{
   public class BC3 {…}
   public class BC4{…}
}

    这两个装配件中都有N1和N2两个命名空间,而且各声明了两个类,这样是完全可以的,然后我们在一个应用程序中引用装配件A,那么在这个应用程序中,我们能看到N1下面的类为AC1和AC2,N2下面的类为AC3和AC4。
    接着我们去掉对A的引用,加上对B的引用,那么我们在这个应用程序下能看到的N1下面的类变成了BC1和BC2,N2下面也一样。
    如果我们同时引用这两个装配件,那么N1下面我们就能看到四个类:AC1、AC2、BC1和BC2。

    到这里,我们可以清楚一个概念了,命名空间只是说明一个类型是那个族的,比如有人是汉族、有人是回族;而装配件表明一个类型住在哪里,比如有人住在北京、有人住在上海;那么北京有汉族人,也有回族人,上海有汉族人,也有回族人,这是不矛盾的。

    上面我们说了,装配件是一个类型居住的地方,那么在一个程序中要使用一个类,就必须告诉编译器这个类住在哪儿,编译器才能找到它,也就是说必须引用该装配件。
    那么如果在编写程序的时候,也许不确定这个类在哪里,仅仅只是知道它的名称,就不能使用了吗?答案是可以,这就是反射了,就是在程序运行的时候提供该类型的地址,而去找到它。
有兴趣的话,接着往下看吧。

3、运行期得到类型信息有什么用
    有人也许疑问,既然在开发时就能够写好代码,干嘛还放到运行期去做,不光繁琐,而且效率也受影响。
这就是个见仁见智的问题了,就跟早绑定和晚绑定一样,应用到不同的场合。有的人反对晚绑定,理由是损耗效率,但是很多人在享受虚函数带来的好处的时侯还没有意识到他已经用上了晚绑定。这个问题说开去,不是三言两语能讲清楚的,所以就点到为止了。
    我的看法是,晚绑定能够带来很多设计上的便利,合适的使用能够大大提高程序的复用性和灵活性,但是任何东西都有两面性,使用的时侯,需要再三衡量。

接着说,运行期得到类型信息到底有什么用呢?
还是举个例子来说明,很多软件开发者喜欢在自己的软件中留下一些接口,其他人可以编写一些插件来扩充软件的功能,比如我有一个媒体播放器,我希望以后可以很方便的扩展识别的格式,那么我声明一个接口:
public interface IMediaFormat
{
string Extension {get;}
Decoder GetDecoder();
}
这个接口中包含一个Extension属性,这个属性返回支持的扩展名,另一个方法返回一个解码器的对象(这里我假设了一个Decoder的类,这个类提供把文件流解码的功能,扩展插件可以派生之),通过解码器对象我就可以解释文件流。
 那么我规定所有的解码插件都必须派生一个解码器,并且实现这个接口,在GetDecoder方法中返回解码器对象,并且将其类型的名称配置到我的配置文件里面。
 这样的话,我就不需要在开发播放器的时侯知道将来扩展的格式的类型,只需要从配置文件中获取现在所有解码器的类型名称,而动态的创建媒体格式的对象,将其转换为IMediaFormat接口来使用。
 
 这就是一个反射的典型应用。
4、如何使用反射获取类型
    首先我们来看如何获得类型信息。
    获得类型信息有两种方法,一种是得到实例对象
    这个时侯我仅仅是得到这个实例对象,得到的方式也许是一个object的引用,也许是一个接口的引用,但是我并不知道它的确切类型,我需要了解,那么就可以通过调用System.Object上声明的方法GetType来获取实例对象的类型对象,比如在某个方法内,我需要判断传递进来的参数是否实现了某个接口,如果实现了,则调用该接口的一个方法:

public void Process( object processObj )
{
 Type t = processsObj.GetType();
 if( t.GetInterface(“ITest”) !=null )
          …
}

    另外一种获取类型的方法是通过Type.GetType以及Assembly.GetType方法,如:
       Type t = Type.GetType(“System.String”);
    需要注意的是,前面我们讲到了命名空间和装配件的关系,要查找一个类,必须指定它所在的装配件,或者在已经获得的Assembly实例上面调用GetType。
    本装配件中类型可以只写类型名称,另一个例外是mscorlib.dll,这个装配件中声明的类型也可以省略装配件名称(.Net装配件编译的时候,默认都引用了mscorlib.dll,除非在编译的时候明确指定不引用它),比如:
     System.String是在mscorlib.dll中声明的,上面的Type t = Type.GetType(“System.String”)是正确的
     System.Data.DataTable是在System.Data.dll中声明的,那么:
 Type.GetType(“System.Data.DataTable”)就只能得到空引用。
     必须:
 Type t = Type.GetType("System.Data.DataTable,System.Data,Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089");
     这样才可以,大家可以看下面这个帖子:
        http://search.csdn.net/Expert/topic/2210/2210762.xml?temp=.1919977
     qqchen的回答很精彩


5、如何根据类型来动态创建对象
    System.Activator提供了方法来根据类型动态创建对象,比如创建一个DataTable:

Type t = Type.GetType("System.Data.DataTable,System.Data,Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089");

DataTable table = (DataTable)Activator.CreateInstance(t);

  例二:根据有参数的构造器创建对象
namespace TestSpace {
public class TestClass
{
   private string _value;
   public TestClass(string value) {
 _value=value;
   }
}
}

Type t = Type.GetType(“TestSpace.TestClass”);
Object[] constructParms = new object[] {“hello”}; //构造器参数
TestClass obj = (TestClass)Activator.CreateInstance(t,constructParms);

把参数按照顺序放入一个Object数组中即可
6、如何获取方法以及动态调用方法
namespace TestSpace
{
   public class TestClass {
     private string _value;
     public TestClass() {
     }
     public TestClass(string value) {
 _value = value;
     }

     public string GetValue( string prefix ) {
 if( _value==null )
     return "NULL";
 else
     return prefix+" : "+_value;
      }

      public string Value {
 set {
  _value=value;
 }
 get {
  if( _value==null )
   return "NULL";
  else
   return _value;
 }
      }
   }
}

    上面是一个简单的类,包含一个有参数的构造器,一个GetValue的方法,一个Value属性,我们可以通过方法的名称来得到方法并且调用之,如:

//获取类型信息
Type t = Type.GetType("TestSpace.TestClass");
//构造器的参数
object[] constuctParms = new object[]{"timmy"};
//根据类型创建对象
object dObj = Activator.CreateInstance(t,constuctParms);
//获取方法的信息
MethodInfo method = t.GetMethod("GetValue");
//调用方法的一些标志位,这里的含义是Public并且是实例方法,这也是默认的值
BindingFlags flag = BindingFlags.Public | BindingFlags.Instance;
//GetValue方法的参数
object[] parameters = new object[]{"Hello"};
//调用方法,用一个object接收返回值
object returnValue = method.Invoke(dObj,flag,Type.DefaultBinder,parameters,null);

    属性与方法的调用大同小异,大家也可以参考MSDN

7、动态创建委托
    委托是C#中实现事件的基础,有时候不可避免的要动态的创建委托,实际上委托也是一种类型:System.Delegate,所有的委托都是从这个类派生的
    System.Delegate提供了一些静态方法来动态创建一个委托,比如一个委托:

namespace TestSpace {
   delegate string TestDelegate(string value);
   public class TestClass {
 public TestClass() {
         }
         public void GetValue(string value) {
             return value;
         }
    }
}
 
使用示例:
TestClass obj = new TestClass();
  
//获取类型,实际上这里也可以直接用typeof来获取类型
Type t = Type.GetType(“TestSpace.TestClass”);
//创建代理,传入类型、创建代理的对象以及方法名称
TestDelegate method = (TestDelegate)Delegate.CreateDelegate(t,obj,”GetValue”);

String returnValue = method(“hello”);

 

 

 

 

 

如果您发现自己在苦苦维持强类型化数据结构的同时,又冀望于最大化运行时灵活性,那么您大概会愿意考虑反射,以及它如何改善您的软件。在本专栏中,我将探讨 Microsoft® .NET Framework 中的 System.Reflection 命名空间,以及它如何为您的开发体验提供助益。我将从一些简单的示例开始,最后将讲述如何处理现实世界中的序列化情形。在此过程中,我会展示反射和 CodeDom 如何配合工作,以有效处理运行时数据。

在深入探究 System.Reflection 之前,我想先讨论一下一般的反射编程。首先,反射可定义为由一个编程系统提供的任何功能,此功能使程序员可以在无需提前了解其标识或正式结构的情况下检查和操作代码实体。这部分内容很多,我将逐一展开说明。

首先,反射提供了什么呢?您能用它做些什么呢?我倾向于将典型的以反 射为中心的任务分为两类:检查和操作。检查需要分析对象和类型,以收集有关其定义和行为的结构化信息。除了一些基本规定之外,通常这是在事先不了解它们的 情况下进行的。(例如,在 .NET Framework 中,任何东西都继承自 System.Object,并且一个对象类型的引用通常是反射的一般起点。)

操作利用通过检查收集到的信息动态地调用代码,创建已发现类型的新实 例,或者甚至可以轻松地动态重新结构化类型和对象。需要指出的一个要点是,对于大多数系统,在运行时操作类型和对象,较之在源代码中静态地进行同等操作, 会导致性能降低。由于反射的动态特性,因此这是个必要的取舍,不过有很多技巧和最佳做法可以优化反射的性能(有关优化使用反射的更多深入信息,请参见 msdn.microsoft.com/msdnmag/issues/05/07/Reflection)。

那么,什么是反射的目标呢?程序员实际检查和操作什么呢?在我对反射的定义中,我用了“代码实体”这个新术语,以强调一个事实:从程序员的角度来说,反射技术有时会使传统对象和类型之间的界限变得模糊。例如,一个典型的以反射为中心的任务可能是:

 

  1. 从对象 O 的句柄开始,并使用反射获得其相关定义(类型 T)的句柄。
  2. 检查类型 T,获得它的方法 M 的句柄。
  3. 调用另一个对象 O’(同样是类型 T)的方法 M。

请注意,我在从一个实例穿梭到它的底层类型,从这一类型到一个方法, 之后又使用此方法的句柄在另一个实例上调用它 — 显然这是在源代码中使用传统的 C# 编程技术无法实现的。在下文中探讨 .NET Framework 的 System.Reflection 之后,我会再次通过一个具体的例子来解释这一情形。

某些编程语言本身可以通过语法提供反射,而另一些平台和框架(如 .NET Framework)则将其作为系统库。不管以何种方式提供反射,在给定情形下使用反射技术的可能性相当复杂。编程系统提供反射的能力取决于诸多因素:程 序员很好地利用了编程语言的功能表达了他的概念吗?编译器是否在输出中嵌入足够的结构化信息(元数据),以方便日后的解读?有没有一个运行时子系统或主机 解释器来消化这些元数据?平台库是否以对程序员有用的方式,展示此解释结果?

如果您头脑中想象的是一个复杂的、面向对象类型的系统,但在代码中却 表现为简单的、C 语言风格的函数,而且没有正式的数据结构,那么显然您的程序不可能动态地推断出,某变量 v1 的指针指向某种类型 T 的对象实例。因为毕竟类型 T 是您头脑中的概念,它从未在您的编程语句中明确地出现。但如果您使用一种更为灵活的面向对象语言(如 C#)来表达程序的抽象结构,并直接引入类型 T 的概念,那么编译器就会把您的想法转换成某种日后可以通过合适的逻辑来理解的形式,就象公共语言运行时 (CLR) 或某种动态语言解释器所提供的一样。

反射完全是动态、运行时的技术吗?简单的说,不是这样。整个开发和执 行周期中,很多时候反射对开发人员都可用且有用。一些编程语言通过独立编译器实现,这些编译器将高级代码直接转换成机器能够识别的指令。输出文件只包括编 译过的输入,并且运行时没有用于接受不透明对象并动态分析其定义的支持逻辑。这正是许多传统 C 编译器的情形。因为在目标可执行文件中几乎没有支持逻辑,因此您无法完成太多动态反射,然而编译器会不时提供静态反射 — 例如,普遍运用的 typeof 运算符允许程序员在编译时检查类型标识。

另一种完全不同的情况是,解释性编程语言总是通过主进程获得执行(脚 本语言通常属于此类)。由于程序的完整定义是可用的(作为输入源代码),并跟完整的语言实现结合在一起(作为解释器本身),因此所有支持自我分析所需的技 术都到位了。这种动态语言频繁地提供全面反射功能,以及一组用于动态分析和操作程序的丰富工具。

.NET Framework CLR 和它的承载语言如 C# 属于中间形态。编译器用来把源代码转换成 IL 和元数据,后者与源代码相比虽属于较低级别或者较低“逻辑性”,但仍然保留了很多抽象结构和类型信息。一旦 CLR 启动和承载了此程序,基类库 (BCL) 的 System.Reflection 库便可以使用此信息,并返回关于对象类型、类型成员、成员签名等的信息。此外,它也可以支持调用,包括后期绑定调用。


.NET 中的反射

要在用 .NET Framework 编程时利用反射,您可以使用 System.Reflection 命名空间。此命名空间提供封装了很多运行时概念的类,例如程序集、模块、类型、方法、构造函数、字段和属性。图 1 中的表显示,System.Reflection 中的类如何与概念上运行时的对应项对应起来。

尽管很重要,不过 System.Reflection.Assembly 和 System.Reflection.Module 主要用于定位新代码并将其加载到运行时。本专栏中,我暂不讨论这些部分,并且假定所有相关代码都已经加载。

要检查和操作已加载代码,典型模式主要是 System.Type。通常,您从获得一个所关注运行时类别的 System.Type 实例开始(通过 Object.GetType)。接着您可以使用 System.Type 的各种方法,在 System.Reflection 中探索类型的定义并获得其它类的实例。例如,如果您对某特定方法感兴趣,并希望获得此方法的一个 System.Reflection.MethodInfo 实例(可能通过 Type.GetMethod)。同样,如果您对某字段感兴趣,并希望获得此字段的一个 System.Reflection.FieldInfo 实例(可能通过 Type.GetField)。

一旦获得所有必要的反射实例对象,即可根据需要遵循检查或操作的步骤继续。检查时,您在反射类中使用各种描述性属性,获得您需要的信息(这是通用类型吗?这是实例方法吗?)。操作时,您可以动态地调用并执行方法,通过调用构造函数创建新对象,等等。


检查类型和成员

让我们跳转到一些代码中,探索如何运用基本反射进行检查。我将集中讨论类型分析。从一个对象开始,我将检索它的类型,而后考察几个有意思的成员(请参见图 2)。

首先需要注意的是,在类定义中,乍看起来说明方法的篇幅比我预期的要 多很多。这些额外的方法是从哪里来的呢?任何精通 .NET Framework 对象层次结构的人,都会识别从通用基类 Object 自身继承的这些方法。(事实上,我首先使用了 Object.GetType 检索其类型。)此外,您可以看到属性的 getter 函数。现在,如果您只需要 MyClass 自身显式定义的函数,该怎么办呢?换句话说,您如何隐藏继承的函数?或者您可能只需要显式定义的实例函数?

随便在线看看 MSDN®,就会发现大家都愿意使用 GetMethods 第二个重载方法,它接受 BindingFlags 参数。通过结合来自 BindingFlags 枚举中不同的值,您可以让函数仅返回所需的方法子集。替换 GetMethods 调用,代之以:

GetMethods(BindingFlags.Instance | BindingFlags.DeclaredOnly | 
BindingFlags.Public)

结果是,您得到以下输出(注意这里不存在静态帮助器函数和继承自 System.Object 的函数)。

Reflection Demo Example 1
Type Name: MyClass
Method Name: MyMethod1
Method Name: MyMethod2
Method Name: get_MyProperty
Property Name: MyProperty

如果您事先知道类型名称(完全限定)和成员,又该如何?您如何完成从枚举类型向检索类型的转换?图 3 中的示例显示了如何通过 Object.GetType 和 Type.GetMethod,使用描述类型信息的字符串文字,检索实际代码对应项。有了前两个示例中的代码,您已经有了能够实现基元类浏览器的基本组 件。通过名称您可以找到一个运行时实体,然后枚举其各种相关属性。


动态调用代码

迄今为止,我已经获得运行时对象的句柄(如类型和方法),仅作描述用,例如输出它们的名称。但是如何做得更多呢?如何实际调用某个方法呢?图 4 显示了如何获得某类型成员的 MethodInfo,然后使用 MethodInfo.Invoke 实际动态调用此方法。

此例的几个要点是:首先,从一个 MyClass, mc1 实例检索一个 System.Type 实例,然后,从该类型检索一个 MethodInfo 实例。最后,当调用 MethodInfo 时,通过把它作为调用的第一个参数来传递,将其绑定到另一个 MyClass (mc2) 实例中。

前面讲过,对于您预期在源代码中见到的类型和对象使用之间的区别,这 个示例使这种区别变得模糊。逻辑上,您检索了一个方法的句柄,然后调用该方法,就象它属于一个不同的对象一样。对于熟悉函数式编程语言的程序员来说,这可 能轻而易举;但对于只熟悉 C# 的程序员来说,要分离对象实现和对象实例化,可能就不是那么直观了。


组合在一起

至此我已经探讨过检查和调用的基本原理,接下来我会用具体的例子把它 们组合在一起。设想您希望交付一个库,带有必须处理对象的静态帮助器函数。但在设计的时候,您对这些对象的类型没有任何概念!这要看函数调用方的指示,看 他希望如何从这些对象中提取有意义的信息。函数将接受一个对象集合,和一个方法的字符串描述符。然后它将遍历该集合,调用每个对象的方法,用一些函数聚合 返回值(请参见图 5)。

就此例而言,我要声明一些约束条件。首先,字符串参数描述的方法(必 须由每个对象的底层类型实现)不会接受任何参数,并将返回一个整数。代码将遍历对象集合,调用指定的方法,逐步计算出所有值的平均值。最后,因为这不是生 产代码,在求和的时候我不用担心参数验证或整数溢出。

在浏览示例代码时,可以看到主函数与静态帮助器 ComputeAverage 之间的协议除了对象自身的通用基类之外,并不依赖任何类型信息。换句话说,您可以彻底改变正在传送的对象的类型和结构,但只要总是能使用字符串描述一个方 法,且该方法返回整数,ComputeAverage 就可以正常工作!

需要注意的一个关键问题跟隐藏在最后这个例子中的 MethodInfo(一般反射)有关。注意,在 ComputeAverage 的 foreach 循环中,代码只从集合中的第一个对象中抓取一个 MethodInfo,然后绑定用于所有后续对象的调用。正如编码所示,它运行良好 — 这是 MethodInfo 缓存的一个简单例子。但此处有一个根本性的局限。MethodInfo 实例仅能由其检索对象同等层级类型的实例调用。因为传入了 IntReturner 和 SonOfIntReturner(继承自 IntReturner)的实例,才能这样运行。

在示例代码中,已经包含了名为 EnemyOfIntReturner 的类,它实现了与其他两个类相同的基本协议,但并没有共享任何常见共享类型。换句话说,该接口逻辑上等同,但在类型层级上没有重叠。要探讨 MethodInfo 在该情形下的使用,请尝试向集合添加其他对象,通过“new EnemyOfIntReturner(10)”得到一个实例,再次运行示例。您会遇到一个异常,指出 MethodInfo 不能用于调用指定的对象,因为它和获得 MethodInfo 时的原始类型完全无关(即使方法名称和基本协议是等同的)。要使您的代码达到生产水准,您需要做好遇到这一情形的准备。

一个可能的解决方案可以是通过自己分析所有传入对象的类型,保留对其 共享的类型层级(如果有)的解释。如果下一对象的类型与任意已知类型层级相异,就需要获取和存储一个新的 MethodInfo。另一解决方案是捕获 TargetException,并重新获取一个 MethodInfo 实例。这里提到的两种解决方案都各有其优缺点。Joel Pobar 为本杂志 2007 五月期写过一篇优秀的文章,内容关于 MethodInfo 缓冲和我所极力推荐的反射性能。

希望此示例演示的向应用程序或框架中添加反射,可以为日后的自定义或 可扩展性增加更多的灵活性。不可否认,较之本机编程语言中的同等逻辑,使用反射可能会有些繁琐。如果您感到对您或您的客户来说,向代码中添加基于反射的后 期绑定过于麻烦(毕竟他们需要以某种方式在您的框架中说明他们的类型和代码),那么可能仅需要适度的灵活性以取得某种平衡。


序列化的高效类型处理

至此我们已通过若干示例讲述了 .NET 反射的基本原理,接下来让我们看一下现实世界中的情形。如果您的软件通过 Web 服务或其他进程外远程技术与其他系统进行交互,那么您很可能已经遇到序列化问题。序列化本质上是将活动的、占用内存的对象,转变成适合线上传输或磁盘存储 的数据格式。

.NET Framework 中的 System.Xml.Serialization 命名空间提供了拥有 XmlSerializer 的强大序列化引擎,它可以使用任意托管对象,并将其转换成 XML(日后也可将 XML 数据转换回类型化的对象实例,这一过程称之为反序列化)。XmlSerializer 类是一种强大的、企业就绪的软件片断,如果您在项目中面临序列化问题,它将是您的首选。但为了教学目的,我们来探讨如何实现序列化(或者其他类似的运行时 类型处理实例)。

设想情形:您正在交付一个框架,需要使用任意用户类型的对象实例,并将其转换成某种智能型数据格式。例如,假定有一个驻留内存的对象,类型为如下所示的 Address:

(pseudocode)
class Address
{
AddressID id;
String Street, City;
StateType State;
ZipCodeType ZipCode;
}

如何生成适当的数据表示形式以方便日后使用?或许一个简单的文本呈现将解决这一问题:

Address: 123
Street: 1 Microsoft Way
City: Redmond
State: WA
Zip: 98052

如果事先完全了解需要转换的正式数据类型(例如自己编写代码时),事情就变得非常简单:

foreach(Address a in AddressList)
{
Console.WriteLine(“Address:{0}”, a.ID);
Console.WriteLine(“/tStreet:{0}”, a.Street);
... // and so on
}

然而,如果预先不知道在运行时会遇到的数据类型,情况会变得十分有趣。您如何编写象这样的一般框架代码?

MyFramework.TranslateObject(object input, MyOutputWriter output)

首先,您需要决定哪些类型成员对序列化有用。可能的情况包括仅捕获特 定类型的成员,例如基元系统类型,或提供一种机制以供类型作者说明哪些成员需要被序列化,例如在类型成员上使用自定义属性作为标记)。您仅可以捕获特定类 型的成员,例如基元系统类型,或类型作者能够说明哪些成员需要被序列化(可能的方法是在类型成员上使用自定义属性作为标记)。

一旦记录清楚需要转换的数据结构成员,您接着需要做的是编写逻辑,从传入的对象枚举和检索它们。反射在这里担负了繁重的任务,让您既可以查询数据结构又可以查询数据值。

出于简单性考虑,我们来设计一个轻型转换引擎,得到一个对象,获取所有其公共属性值,通过直接调用 ToString 将它们转换成字符串,然后将这些值序列化。对于一个名为“input”的给定对象,算法大致如下:

 

  1. 调用 input.GetType 以检索 System.Type 实例,该实例描述了 input 的底层结构。
  2. 用 Type.GetProperties 和适当的 BindingFlags 参数,将公共属性作为 PropertyInfo 实例检索。
  3. 使用 PropertyInfo.Name 和 PropertyInfo.GetValue,将属性作为键-值对检索。
  4. 在每个值上调用 Object.ToString 将其(通过基本方式)转化为字符串格式。
  5. 将对象类型的名称和属性名称、字符串值的集合打包成正确的序列化格式。

这一算法明显简化了事情,同时也抓住了得到运行时数据结构,并将其转化为自描述型数据的要旨。但这里有一个问题:性能。之前提到,反射对于类型处理和值检索的成本都很高。本示例中,我在每个提供类型的实例中执行了完整的类型分析。

如果以某种方式可以捕获或保留您对于类型结构的理解,以便日后不费力 地检索它,并有效处理该类型的新实例;换句话说,就是往前跳到示例算法中的步骤 #3?好消息是,利用 .NET Framework 中的功能,完全可能做到这一点。一旦您理解了类型的数据结构,便可以使用 CodeDom 动态生成绑定到该数据结构的代码。您可以生成一个帮助器程序集,其中包含帮助器类和引用了传入类型并直接访问其属性的方法(类似托管代码中的任何其他属 性),因此类型检查只会对性能产生一次影响。

现在我将修正这一算法。新类型:

 

  1. 获得对应于该类型的 System.Type 实例。
  2. 使用各种 System.Type 访问器检索架构(或至少检索对序列化有用的架构子集),例如属性名称、字段名称等。
  3. 使用架构信息生成帮助器程序集(通过 CodeDom),该程序集与新类型相链接,并有效地处理实例。
  4. 在帮助器程序集中使用代码,提取实例数据。
  5. 根据需要序列化数据。

对于给定类型的所有传入数据,可以往前跳到步骤 #4,较之显式检查每一实例,这么做可以获得巨大的性能提升。

我开发了一个名为 SimpleSerialization 的基本序列化库,它用反射和 CodeDom(本专栏中可下载)实现了这一算法。主要组件是一个名为 SimpleSerializer 的类,是用户用一个 System.Type 实例构造所得。在构造函数中,新的 SimpleSerializer 实例会分析给定的类型,利用帮助器类生成一个临时程序集。该帮助器类会紧密绑定到给定的数据类型,而且对实例的处理方式就象自己在完全事先了解类型的情况 下编写代码那样。

SimpleSerializer 类有如下布局:

class SimpleSerializer
{
public class SimpleSerializer(Type dataType);

public void Serialize(object input, SimpleDataWriter writer);
}

简单地令人惊叹!构造函数承担了最繁重的任务:它使用反射来分析类型结构,然后用 CodeDom 生成帮助器程序集。SimpleDataWriter 类只是用来阐明常见序列化模式的数据接收器。

要序列化一个简单的 Address 类实例,用下面的伪代码即可完成任务:

SimpleSerializer mySerializer = new SimpleSerializer(typeof(Address));
SimpleDataWriter writer = new SimpleDataWriter();
mySerializer.Serialize(addressInstance, writer);

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值