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