序列化的高效类型处理
至此我们已通过若干示例讲述了 .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”的给定对象,算法大致如下:
调用 input.GetType 以检索 System.Type 实例,该实例描述了 input 的底层结构。
用 Type.GetProperties 和适当的 BindingFlags 参数,将公共属性作为 PropertyInfo 实例检索。
使用 PropertyInfo.Name 和 PropertyInfo.GetValue,将属性作为键-值对检索。
在每个值上调用 Object.ToString 将其(通过基本方式)转化为字符串格式。
将对象类型的名称和属性名称、字符串值的集合打包成正确的序列化格式。
这一算法明显简化了事情,同时也抓住了得到运行时数据结构,并将其转化为自描述型数据的要旨。但这里有一个问题:性能。之前提到,反射对于类型处理和值检索的成本都很高。本示例中,我在每个提供类型的实例中执行了完整的类型分析。
如果以某种方式可以捕获或保留您对于类型结构的理解,以便日后不费力地检索它,并有效处理该类型的新实例;换句话说,就是往前跳到示例算法中的步骤 #3?好消息是,利用 .NET Framework 中的功能,完全可能做到这一点。一旦您理解了类型的数据结构,便可以使用 CodeDom 动态生成绑定到该数据结构的代码。您可以生成一个帮助器程序集,其中包含帮助器类和引用了传入类型并直接访问其属性的方法(类似托管代码中的任何其他属性),因此类型检查只会对性能产生一次影响。
现在我将修正这一算法。新类型:
获得对应于该类型的 System.Type 实例。
使用各种 System.Type 访问器检索架构(或至少检索对序列化有用的架构子集),例如属性名称、字段名称等。
使用架构信息生成帮助器程序集(通过 CodeDom),该程序集与新类型相链接,并有效地处理实例。
在帮助器程序集中使用代码,提取实例数据。
根据需要序列化数据。
对于给定类型的所有传入数据,可以往前跳到步骤 #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);
结束 强烈建议您亲自试用一下示例代码,尤其是 SimpleSerialization 库。我在 SimpleSerializer 一些有趣的部分都添加了注释,希望能够有所帮助。当然,如果您需要在产品代码中进行严格的序列化,那么确实要依靠 .NET Framework 中提供的技术(例如 XmlSerializer)。但如果您发现在运行时需要使用任意类型并能高效处理它们,我希望您采用我的 SimpleSerialization 库作为自己的方案。
查看本文来源