序列化是将对象或对象图转换成字节流的过程,反序列化是将字节流转换回对象图的过程。在对象和字节流之间转换是很有用的机制。
1 应用程序的状态(对象图)可轻松保存到磁盘文件或数据库中,并在应用程序下次运行时恢复。ASP.NET就是利用序列化和反序列来保存和还原会话状态。
2 一组对象可轻松复制到系统的剪贴板,再粘贴回同一个或另一个应用程序。事实上,windows窗体和windows presentation foundation(WPF)就利用了这个功能。
3 一组对象可克隆并放到一边作为“备份”;与此同时,用户操纵一组“主”对象。
4 一组对象可轻松地通过网络发送给另一台机器上运行的进程。Microsoft .net framework的remoting(远程处理)架构会对按值封送(marshaled by value)的对象进行序列化和反序列化。这个技术还可跨AppDomain边界发送对象。
除了上述应用,一旦将对象序列化成内存中的字节流,就可方便地以一些更有用的方式处理数据,比如进行加密和压缩。
由于序列化如此有用,所以许多程序员耗费了大量时间写代码执行这些操作。历史上,这种代码很难编写,相等繁琐,还容易出错。开发人员需要克服的难题包括通信协议、客户端/服务器数据类型不匹配(比如低位优先/高位优先问题)、错误处理、一个对象引用了其他对象、in和out参数以及由结构构成的数组。
让人高兴的是,.net framework内建了出色的序列化和反序列化支持。上述所有难题都迎刃而解,而且.net framework是在后台悄悄帮你解决的。开发者现在只需负责序列化之前和反序列化之后的对象处理,中间过程由.net framework负责。
本章解释了.net framework如何公开它的序列化和序列化服务。对于几乎所有数据类型,这些服务的默认行为已经足够。也就是说,几乎不需要做任何工作就可以使自己的类型“可序列化”。但对于少量类型,序列化服务的默认行为是不够的。幸好,序列化服务的扩展性极佳,本章将解释如何利用这些扩展性机制,在序列化或反序列化对象时采取一些相当强大的操作。
注意:本章重点在于clr的运行时序列化技术。这种技术对clre数据类型有很深刻的理解,能将对象的所有公共、受保护、内部甚至私有字段序列化到压缩的二进制流中,从而获得很好的性能。
序列化/反序列化快速入门
下面先来看一些代码
static void Main(string[] args) { //创建对象图以便把它们序列化到流中 var objectGraph=new List<string> {"Jeff","Kristin","Aidan"}; Stream stream = SerializeToMemory(objectGraph); //为了掩饰,将一切都重置 stream.Position = 0; objectGraph = null; //反序列化对象,证明它能工作 objectGraph = (List<string>) DeserializeFromMemory(stream); foreach (var s in objectGraph) { Console.WriteLine(s); } } private static MemoryStream SerializeToMemory(object objectGraph) { //构造流来容纳序列化对象 MemoryStream stream=new MemoryStream(); //构造序列化格式化器来执行所有真正的工作 BinaryFormatter formatter=new BinaryFormatter(); //告诉格式化器将对象序列化到流中 formatter.Serialize(stream,objectGraph); //将序列化好的对象流返回给调用者 return stream; } private static object DeserializeFromMemory(Stream stream) { //构造序列化格式器来做所有真正的工作 BinaryFormatter formatter=new BinaryFormatter(); //告诉格式化器从流中反序列化对象 return formatter.Deserialize(stream); }
一切视乎都很简单!SerializeToMemory方法构造一个system.io.memoryStream对象。这个对象表明要将序列化好的字节块放在哪儿。然后,方法构造一个BinaryFormatter对象(在System.Runtime.Serialization.Formatters.Binary命名空间中定义)。格式化器是实现了System.Runtime.Serialization.IFormatter接口的类型,它知道如何序列化和反序列化对象图。
序列化对象图只需调用格式化器的serialize方法,并向它传递两样东西:对流对象的引用,以及对想要序列化的对象图的引用。刘对象表示了序列化好的字节应该放到哪里,它可以是从system.io.stream抽象基类派生的任何类型的对象。
格式化器参考对每个对象的类型进行描述的元数据,从而了解如何序列化完整的对象图。序列化时,serialize方法利用反射来查看每个对象的类型中都有哪些实例字段。在这些字段中,任何一个引用了其他对象,格式化器的serialize方法就知道哪些对象也要进行序列化。
格式化器的算法非常智能。他们知道如何确保对象图中的每个对象都只序列化一次。换言之,如果对象图中的两个对象互相引用,格式化器会检测到这一点,每个对象都只序列化一次,避免发生死循环。
在上述代码的SerializeToMemory方法中,当格式化器的serialize方法返回后,memoryStream直接返回给调用者。应用程序可以按照自己希望的任何方式利用这个字节数组的内容。例如,可以把它保存到文件中、复制到剪贴板或者通过网络发送等。
DeserializeFromMemory方法将流反序列化为对象图。该方法币用于序列化对象图的方法还要简单。在代码中,我构造了一个BinaryFormatter,然后调用他的deserialize方法。这个方法获取流作为参数,返回对反序列化好的对象图中的根对象的一个引用。
在内部,格式化器的deserialize方法检查流的内容,构造流中所有对象的实例,并初始化所有这些对象中的字段,使它们具有与当初序列化时相同的值。通常要将deserialize方法返回的对象引用转型为应用程序期待的类型。
下面是一个有趣而实用的方法,它利用序列化创建对象的深拷贝:
private static object DeepClone(object original) { //构造临时内存流 using (MemoryStream stream=new MemoryStream()) { //构造序列化格式器来做所有真正的工作 BinaryFormatter formatter=new BinaryFormatter(); formatter.Context=new StreamingContext(StreamingContextStates.Clone); //将对象图序列化到内存流中 formatter.Serialize(stream,original); //反序列化前,定位到内存流的起始位置 stream.Position = 0; //将对象图反序列化成一组新对象,向调用者返回对象图(深拷贝)的根 return formatter.Deserialize(stream); } }
有几点需要注意。首先,是由你来保证代码为序列化和反序列化使用相同的格式化器。其次,可以将多个对象图序列化到一个流中,这是很有用的一个操作。例如,假定有以下两个类定义:
[Serializable] internal sealed class Customer{} [Serializable] internal sealed class Oreder{}
然后,在应用程序的主要类中定义了以下静态字段:
private static Lists_customers=new List (); private static List s_pendingOrders=new List (); private static List s_processedOrders=new List ();
现在,可利用如下所示方法将应用程序的状态序列化到单个流中:
private static void SaveApplicationState(Stream stream) { //构造序列化格式器来做所有真正的工作 BinaryFormatter formatter=new BinaryFormatter(); //序列化我们的应用程序的完整状态 formatter.Serialize(stream,s_customers); formatter.Serialize(stream,s_pendingOrders); formatter.Serialize(stream,s_processedOrders); }
要重新构建应用程序的状态,可以使用如下所示的一个方法反序列化状态:
private static void RestoreApplicationState(Stream stream) { //构造序列化格式器来做所有真正的工作 BinaryFormatter formatter=new BinaryFormatter(); //反序列化我们的应用程序的完整状态(和序列化时的顺序一样) s_customers=( List)formatter.Deserialize(stream); s_pendingOrders=( List )formatter.Deserialize(stream); s_processedOrders=( List )formatter.Deserialize(stream); }
最后一个注意事项与程序集有关。序列化对象时,类型的全名和类型定义程序集的全名会被写入流。BinaryFormatter默认输出程序集的完整表示,其中包括程序集的文件名(无扩展名)、版本号、语言文化以及公钥信息。反序列化对象是,格式化器首先获取程序集标识信息,并通过调用system.reflection.assembly的load方法确保程序集已加载到正在执行的appdomain中。
程序集加载好之后,格式化器在程序集中查找与要反序列化的对象匹配的类型。找不到匹配类型就抛出异常,不再对更多的对象进行反序列化。找到匹配的类型,就创建类型的实例,并用流中包含的值对其字段字段进行初始化。如果类型中的字段与流中读取的字段名不完全匹配,就抛出SerializationException异常,不再对更多的对象进行反序列化。
本节讲述了序列化与反序列化对象图的基础知识。之后我们将讨论如何定义自己的可序列化类型。
使类型可序列化
设计类型时,设计人员必须郑重地决定是否允许类型的实例序列化。类型默认是不可序列化的。开发者必须向类型应用定制特性system.serializableAttribute。任何对象不可序列化,格式化器的serialize方法都会抛出异常。
注意:考虑到性能,在序列化之前,格式化器不会验证对象图中的所有对象都能序列化。所以在抛出异常之前,完全有可能已经有一部分对象序列化到流中。如果发生这种情况,流中就会包含已损坏的数据。
注意:枚举类型和委托类型总是可序列化的,所以不必显示应用serializableAttribute特性。
序列化会读取对象的所有字段,不管这些字段声明为public,protercted,private还是internal。
控制序列化和反序列化
类型可能定义了一些不应序列化的实例字段。一般有如下两个原因
1 字段含有反序列化后变得无效的信息。例如,对象包含windows内核对象(如文件、进程、线程、互斥体、事件、信号量等)的句柄,那么在反序列化到另一个进程或另一台机器之后,就会失去意义。因为windows内核对象是跟进程相关的值。
2 字段含有很容易计算的信息。这时要选出那些无须序列化的字段,减少需要传输的数据。
可以使用[NonSerialized]特性指出类型中不应序列化的字段。注意,该特性只能应用于类型中的字段,而且会被派生类型继承。
但是,如果字段被设置为NonSerialized时,反序列化时,字段会被置为默认值。可以用OnDeserialized特性修正这个问题。
[Serializable] public class MyType { private int x, y; [NonSerialized] private int sum; public MyType(int x,int y) { this.x = x; this.y = y; sum = x + y; } [OnDeserializing] private void OnDeserializing(StreamingContext context) { //为字段设置默认值 } [OnDeserialized] private void OnDeserialized(StreamingContext context) { //为字段设置默认值 sum = x + y; } [OnSerializing] private void OnSerializing(StreamingContext context) { // } [OnSerialized] private void OnSerialized(StreamingContext context) { // } }
将方法名声明为private,一面它被普通的代码调用。格式化运行时有充足的安全权限,所以能调用私有方法。
如果序列化类型的实例,在类型中添加新字段,然后试图反序列化不包含新字段的对象,格式化器会抛出异常。这非常不利于版本控制,因为我们经常都要在类型的新版本中添加新字段。幸好,这时可以利用OptionalFieldAttribute特性。然后,当格式化器看到该特性应用于一个字段时,就不会因为流中的数据不包含这个字段而抛出异常。
格式化器如何序列化类型实例
本节将深入讨论格式化器如何序列化对象的字段。为了简化格式化器的操作,fcl在system.runtime.serialization命名空间提供了一个formatterServices类型。该类型只包含静态方法,而且该类型不能实例化。以下步骤描述了格式化器如何自动序列化类型应用serializable特性的对象。
1 格式化器调用FormatterService的GetSerializableMembers方法
public static MemberInfo[] GetSerializableMembers(Type type, StreamingContext context);
这个方法利用反射获取类型的public和private实力字段,方法返回由MemberInfo对象构成的数组,其中每个元素都对应一个可序列化的实力字段。
2 对象被序列化,system.reflection.memberInfo对象数组传给FormatterService的静态方法GetObjectData
public static object[] GetObjectData(object obj, MemberInfo[] members);
这个方法返回一个一个object数组,其中每个元素都表示了被序列号的那个对象中的一个字段的值。这个object数组和memberInfo数组是并行的;换言之,object数组中的元素0是memberInfo数组中的元素0锁表示的那个成员的值。
3 格式化器将程序集标识和类型的完整名称写入流中。
4 格式化器然后遍历两个数组中的元素,将每个成员的名称和值写入流中。
以下步骤描述了格式化器如何自动反序列化类型应用serializable特性的对象。
1 格式化器从流中读取程序集标识和完整类型名称。如果程序集当前没有加载到appDomain中,就加载它。如果程序集不能加载,就抛出异常,对象不能反序列化。如果程序集已加载,格式化器将程序集标识信息和类型全名传给FormatterService的静态方法GetTypeFromAssembly
public static Type GetTypeFromAssembly(Assembly assem, string name);
这个方法返回一个system.type对象,它代表要反序列化的那个对象的类型。
2 格式化器调用FormatterService的静态方法GetUninitializedObject
public static object GetUninitializedObject(Type type);
这个方法为一个新对象分配内存,但不为对象调用构造器。然而,对象的所有字节都被初始化为成null或0.
3 格式化器现在构造并初始化一个memberInfo数组,具体做法和前面一样,都是调用FormatterService的GetSerializableMembers方法。这个方法返回序列化好、现在需要反序列化的一组字段。
4 格式化器根据流中包含的数据创建并初始化一个object数组。
5 将新分配对象、memberInfo数组以及并行object数组(其中包含字段值)的引用传给FormatterService的PopulateObjectMembers方法。
public static object PopulateObjectMembers(object obj, MemberInfo[] members, object[] data);
这个方法遍历数组,并将每个字段初始化成对应的值。到此为止,对象就算是被彻底反序列化了。
控制序列化/反序列化的数据
本章前面讨论过,控制序列化和反序列过程的最佳方式就是使用OnSerializing等特性。然后,一些极少的情况下,这些特性不能提供你想要的全部控制。此外,格式化器内部使用的是反射,而反射的速度是比较慢的,这会增大序列化和反序列化对象所花的时间。为了对序列化、反序列化的数据进行完全控制,并避免使用反射,你的类型可实现system.runtime.serialization. ISerializable接口,定义如下
public interface ISerializable { void GetObjectData(SerializationInfo info, StreamingContext context); }
这个接口只有一个方法,即GetObjectData。但实现这个接口的大多数类型还实现了一个特殊的构造器。
重要提示:ISerializable接口最大的问题在于,一旦类型实现了它,所有派生类型也必须实现它,而且派生类型必须保证调用基类的GetObjectData方法和特殊构造器。此外,一旦类型实现了该接口,便永远不能删除它,否则会时区与派生类型的兼容性。所以,密封类实现ISerializable接口是最让人放心的。
格式化器序列化对象图时会检查每个对象。如果发现一个对象的类型实现了ISerializable接口,就会忽略所有定制特性,改为构造新的System.Runtime.Serialization.SerializationInfo对象。该对象包含了要为对象序列化的值的集合。
构造SerializationInfo对象时,格式化器要传递两个参数:type和System.Runtime.Serialization.IFormatterConverter。type参数标识要序列化的对象。
构造好并初始化好SerializationInfo对象后,格式化器调用类型的getObjectData方法,向它传递对SerializationInfo对象的引用。GetObjectData调用SerializationInfo类型提供的addValue方法的总舵重载版本之一来指定要序列化的信息。针对要添加的每个数据,都要调用一次addValue。
以下代码展示了dictionary
[Serializable] public class Dictionary: ISerializable, IDeserializationCallback { private SerializationInfo m_siInfo;//只用于反序列化 //用于控制反序列化的特殊构造器(这是ISerializable需要的) [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)] protected Dictionary(SerializationInfo info, StreamingContext context) { //反序列化期间,为OnDeserialization保存SerializationInfo m_siInfo=info; } //用于控制序列化的方法 [SecurityCritical] public virtual void GetObjectData(SerializationInfo info, StreamingContext context) { info.AddValue("",m_version); //...... } //所有key/value对象都反序列化好之后调用的方法 public virtual void IDeserializationCallback.OnDeserialization(object sender) { if (m_siInfo==null) { return;//从不设置,直接返回 } } }
每个Addvalue方法都获取一个string名称和一些数据。数据一般是简单的值类型。然后,还可以在调用addValue时向它传递对一个object的引用。getObjectData添加好所有必要的序列化信息之后,会返回至格式化器。
注意:务必调用AddValue方法的某个重载版本为自己的类型添加序列化信息。
知道了如何设置序列化所需的全部信息之后,再来看反序列化。格式化器从流中提取一个对象时,会为新对象分配内存(FormatterService的静态方法GetUninitializedObject)。最初,这个对象的所有字段都设为0或者null。然后,格式化器检查类型是否实现了Iserializable接口。如果存在这个接口,格式化器就尝试调用一个特殊构造器,他的参数和GetObjectData方法完全一致。
反序列化对象的字段时,应调用和对象序列化时传给AddValue方法的值的类型匹配的get方法。换言之,如果GetObjectData方法调用AddValue时传递的是一个int值,那么在反序列化对象时,应该为同一个值调用getInt32方法。如果值在流中的类型和你试图获取get的类型不符,格式化器会尝试用一个IformatterConverter对象将流中的值类型转型成你指定的类型。
要实现Iserializable但基类型没有实现怎么办?
前面讲过,Iserializable接口的功能非常强大,允许类型完全控制如何对类型的实例进行序列化和反序列化。但这个能力是有代价的:现在,该类型还要负责它的基类型的所有字段的序列化。如果基类型也实现了Iserializable接口,那么对基类型的字段进行序列化时很容易。调用基类型的GetObjectData。
但是如果基类型没有实现Iserializable接口,在这种情况下,派生类必须手动序列化基类的字段。具体的做法是获取它们的值,并把这些值添加到SerializationInfo集合中。然后,在你的特殊构造器中,还必须从集合中取出值,并以某种方式设置基类的字段。
以下代码为简单的示例:
[Serializable] internal class Base { protected string m_name = "Jeff"; public Base(){} } [Serializable] internal class Derived:Base,ISerializable { private DateTime m_date=DateTime.Now; public Derived(){} [SecurityPermission(SecurityAction.Demand,SerializationFormatter = true)] private Derived(SerializationInfo info, StreamingContext context) { //为我们的类和基类获取可序列化的成员集合 Type baseType = this.GetType().BaseType; MemberInfo[] mi = FormatterServices.GetSerializableMembers(baseType, context); //从info对象反序列化基类的字段 for (int i = 0; i < mi.Length; i++) { //获取字段,并把它设为反序列化好的值 FieldInfo fi = (FieldInfo) mi[i]; fi.SetValue(this,info.GetValue(baseType.FullName+"+"+fi.Name,fi.FieldType)); } //反序列化为这个类序列化的值 m_date = info.GetDateTime("Date"); } [SecurityPermission(SecurityAction.Demand,SerializationFormatter =true)] public virtual void GetObjectData(SerializationInfo info, StreamingContext context) { //为这个类序列化希望的值 info.AddValue("Date",m_date); //获取我们的类和基类的可序列化的成员 Type baseType = this.GetType().BaseType; MemberInfo[] mi = FormatterServices.GetSerializableMembers(baseType, context); //将基类的字段序列化到info对象中 for (int i = 0; i < mi.Length; i++) { //为字段名附加基类型全名作为前缀 FieldInfo fi = (FieldInfo) mi[i]; info.AddValue(baseType.FullName+"+"+fi.Name,(FieldInfo) mi[i]); } } public override string ToString() { return string.Format("Name={0},Date={1}", m_name, m_date); } }
流上下文
前面浆果,一组序列化好的对象可以有许多目的地:同一个进程、同一台机器上的不同进程、不同机器上的不同进程等。在一些比较少见的情况下,一个对象可能想知道它要在什么地方反序列化,从而以不同的方式生产它的状态。例如,如果对象中包装了windows信号量对象,如果它知道要反序列化到同一个进程,就可决定对它的内核句柄进行序列化,这是因为内核句柄在一个进程中有效。但如果要反序列化到同一台计算机的不同进程中,就可决定对信号量的字符串名称名称进行序列化。最后,如果要反序列化到不同计算机上的进程,就可决定抛出异常,因为信号量只在一台机器内有效。
本章提到的大量方法都接受一个streamingContext(流上下文)。streamingContext结构是一个非常简单的值类型,2值提供了两个公共只读属性,如下
接受一个streamingContext结构的方法能检查state属性的位标志,判断要序列化/反序列化的对象的来源或目的地。下表展示了可能的位标志值。
知道如何获取这些信息后,接着讨论如何设置。IFormatter接口定义了streamingContext类型的可读写属性context。构造格式化器时,格式化器会初始化它的context属性,将StreamingContextStates设为All,将对额外状态对象的引用设为null。
格式化器构造好之后,就可以使用任何StreamingContextStates位标志来构造一个StreamingContext结构,并可选择传递一个对象引用。现在,在调用格式化器的serialize或deserialize方法之前,你只需要将格式化器的context属性设为这个新的StreamingContext对象。
类型序列化为不同类型以及对象反序列化为不同对象
.net framework的序列化架构是相当的,本节要讨论如何设计类型将自己序列化或反序列化成不同的类型或对象。下面列举了一些有趣的例子。
1 有的类型(比如system.DBNull和system.reflection.Missing)设计为每个appdomain一个实例。经常将这些类型称为单实例(singleton)类型。给定一个DBNull对象引用,序列化和反序列化它不应造成在appdomain中新建一个DBNull对象,反序列化后,返回的引用应指向appdomain中现有的DBNull对象。
2 对某些类型(比如system.type和system.reflection.assembly,以及其他反射类型,例如memberinfo),每个类型、程序集或成员等都只能有一个实例。例如,假定一个数组中的每个元素都引用一个memberInfo对象,其中5个元素引用的都是一个memberInfo对象。序列化和反序列化这个数组后,那5个元素引用的应该还是一个memberInfo对象。除此之外,这些元素引用的memberInfo对象还必须实际对应于appdomain中的一个特定成员。轮询数据库连接对象或者其他任何类型的对象时,么这个功能也是很好用的。
3 对于远程控制的对象,clr序列化与服务器对象有关的信息。在客户端上反序列化时,会造成clr创建一个代理对象。这个代理对象的类型有别于服务器对象的类型,但这对于客户端代码来说是透明的(客户端不需要关心这个问题)。客户端直接在代理对象上调用实例方法。然后,代理代码内部会调用远程发送给服务器,由后者实际执行请求操作。
下面示例代码,它们展示了如何正确地序列化和反序列化单实例类型:
[Serializable] public sealed class Singleton:ISerializable { //这是该类型的一个实例 private static readonly Singleton s_theOneObject=new Singleton(); //这些是实例字段 public string Name = "Jeff"; public DateTime Date=DateTime.Now; //私有构造器,允许这个类型构造单实例 private Singleton(){} //该方法返回对单实例的引用 public static Singleton GetSingleton() { return s_theOneObject; } //序列化一个singleton时调用的方法 //我建议在这里使用一个显式接口方法实现(eimi) [SecurityPermission(SecurityAction.Demand,SerializationFormatter = true)] void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context) { info.SetType(typeof(SingletonSerializationHelper)); } [Serializable] private sealed class SingletonSerializationHelper:IObjectReference { //这个方法在对象(它没有字段)反序列化之后调用 public object GetRealObject(StreamingContext context) { return Singleton.GetSingleton(); } } }
singleton类所代表的类型规定每个appdomain只能存在它的一个实例。以下代码测试singleton的序列化和反序列化代码,保证appDomain中只有singleton类型的一个实例:
static void Main(string[] args) { //创建数组,其中多个元素引用一个singleton对象 Singleton[] al = {Singleton.GetSingleton(), Singleton.GetSingleton()}; Console.WriteLine("Do both elements refer to the same Object?"+(al[0]==al[1])); using (var stream=new MemoryStream()) { BinaryFormatter formatter=new BinaryFormatter(); //先序列化再反序列化数组元素 formatter.Serialize(stream,al); stream.Position = 0; Singleton[] a2 = (Singleton[]) formatter.Deserialize(stream); //证明它的工作和预期一样 Console.WriteLine("Do both elements refer to the same Object?"+(a2[0]==a2[1]));//true Console.WriteLine("Do both elements refer to the same Object?"+(al[0]==a2[0]));//true } }
现在,我们通过分析代码来理解所发生的事情。singleton类型加载到appdomain中时,clr调用它的静态构造器来构造一个singleton对象,并将对它的引用保存到静态字段s_theOneObject中。singleton类没有提供任何公共构造器,这防止了其他任何代码构造该类的其他实例。
序列化数组之后,SingletonSerialization调用格式化器的Deserialize方法。对流进行反序列化时,格式化器尝试反序列化一个SingletonSerializationHelper对象,这是格式化器之前被“欺骗”所序列化的东西。构造好SingletonSerializationHelper对象后,格式化器发现这个类型实现了system.runtime.Serialization. IObjectReference接口。如果类型实现了这个接口,格式化器会调用GetRealObject方法。这个方法返回在对象反序列化好之后你真正想引用的对象。在我的例子中,SingletonSerializationHelper类型让GetRealObject返回对appdomain中已经存在的singleton对象的一个引用。所以,当格式化器的Deserialize方法返回时,a2数组包含两个元素,两者都引用appdomain的singleton对象。用于帮助进行反序列化的SingletonSerializationHelper对象立即变得“不可达”了,将来会被垃圾回收。
序列化代理
前面讨论了如何修改一个类型的实现,控制该类型如何对它本身的实例进行序列化和反序列化。然而,格式化器还允许不是“类型实现的一部分”的代码重写该类型“序列化和反序列化其对象”的方式。应用程序代码之所以要重写(覆盖)类型的行为,主要是出于两方面的考虑。
1 允许开发人员序列化最初没有设计要序列化的类型。
2 允许开发人员提供一种方式将类型的一个版本映射到类型的一个不同的版本。
简单地说,为了使这个机制工作起来,首先要定义一个“代理类型”(surrogate type),它接管对现有类型进行序列化和反序列化的行动。然后,向格式化器等级该代理类型的实例,告诉格式化器代理类型要作用域现有的哪个类型。一旦格式化器要对现有类型的实例进行序列化和反序列化,就调用由你的代理对象定义的方法。下面例子演示这一切如何工作:
序列化代理类型必须实现system.runtime.Seserialization. ISerializationSurrogate接口,如下定义
public interface ISerializationSurrogate { [SecurityCritical] object SetObjectData(object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector); }
假定程序包含一些datetime对象,其中包含用户计算机的本地值。如果想把datetime对象序列化到流中,同时希望值用国际标准时间(世界时)序列化,那么应该如何操作?下面展示了如何定义代理类:
public class UniversalToLocalTimeSerializationSurrogate:ISerializationSurrogate { public void GetObjectData(object obj,SerializationInfo info,StreamingContext context) { //将datetime从本地时间转换成UTC info.AddValue("Date",((DateTime)obj).ToUniversalTime().ToString("u")); } public object SetObjectData(object obj,SerializationInfo info,StreamingContext context,ISurrogateSelector selector) { //将datetime从UTC转换成本地时间 return DateTime.ParseExact(info.GetString("Date"), "u", null).ToLocalTime(); } }
GetObjectData方法在这里的工作方式与ISerializable接口的GetObjectData方法差不多。唯一的区别在于,ISerializationSurrogate的GetObjectData方法要获取一个额外的参数---对要序列化的“真实”对象的引用。上述GetObjectData方法中,这个对象转型为datetime,值从本地时间转换为世界时,并将一个字符串添加到SerializationInfo集合。
SetObjectData方法用于反序列化一个datetime对象。调用这个方法时要向它传递一个SerializationInfo对象引用。SetObjectData从这个集合中获取字符串形式的日期,解析为计算机本地时间。
传给SetObjectData第一个参数的object有点奇怪。在调用setObjectData之前,格式化器分配(调用FormatterService的静态方法GetUninitializedObject)要代理的那个类型的实例。实例的字段全是0/null,而且没有在对象上调用构造器。SetObjectData内部的代码为了初始化这个实例的字段,可以使用传入的SerializationInfo中的值,并让SetObjectData返回null。另外,SetObjectData可以创建一个完全不同的对象,甚至创建不同类型的对象,并返回对新对象的引用。这种情况下,格式化器会忽略对传给SetObjectData对象的任何更改。
序列化/反序列化一个datetime对象时,格式化器怎么知道要用这个ISerializationSurrogate类型呢?一下代码进行测试:
static void Main(string[] args) { using (var stream=new MemoryStream()) { //1 构造所需的格式化器 IFormatter formatter = new BinaryFormatter(); //2 构造一个SurrogateSelector代理选择器对象 SurrogateSelector ss = new SurrogateSelector(); //3 告诉代理选择器为datetime对象使用我们的代理 ss.AddSurrogate(typeof(DateTime),formatter.Context,new UniversalToLocalTimeSerializationSurrogate()); //4 告诉格式化器使用代理选择器 formatter.SurrogateSelector = ss; //创建一个datetime来代表机器上的本地时间,并序列化它 DateTime localTimeBeforeSerialize=DateTime.Now; formatter.Serialize(stream,localTimeBeforeSerialize); //stream将university时间作为一个字符串显示,证明能正常工作 stream.Position = 0; Console.WriteLine(new StreamReader(stream).ReadToEnd()); //反序列化universal时间字符串,并且把它转换成本地datetime stream.Position = 0; DateTime localTimeAfterDeserialize = (DateTime) formatter.Deserialize(stream); //证明它能正确工作 Console.WriteLine("LocalTimeBeforeSerialize ={0}",localTimeBeforeSerialize); Console.WriteLine("LocalTimeAfterDeserialize={0}",localTimeAfterDeserialize); } }
步骤1到步骤4执行完毕后,格式化器就准备好实用已登记的代理类型。调用格式化器的serialize方法时,会在surrogateSelector维护的集合(一个哈希表)中查找(要序列化的)每个对象的类型。如果发现一个匹配,就调用IserializationSurrogate对象的GetObjectData方法来获取应该写入流的信息。
格式化器的deseialize方法在调用时,会在格式化器的surrogateSelector中查找要反序列化的对象的类型。如果发现一个匹配,就调用IserializationSurrogate对象的SetObjectData方法来设置要反序列化的对象中的字段。
surrogateSelector对象在内部维护了一个私有哈希表。调用AddSurrogate时,type和streamingContext构成了哈希表的键(key),对应的值(value)就是IserializationSurrogate对象。如果已经存在和要添加的type/streamingContext相同的一个键,addSurrogate会抛出一个ArgumentException。通过在键中包含一个stramingContext,可以登记一个代理类型对象,它知道如何将datetime对象序列化/反序列化到一个文件中;再登记一个不同的代理对象,它知道如何将datetime对象序列化/反序列化到一个不同的进程中。
代理选择器链
多个SurrogateSelector对象可连接到一起。例如,可以让一个SurrogateSelector对象维护一组序列化代理,这些序列化代理(Surrogate)用于将类型序列化成代理(proxy),以便通过网络传送,或者跨越不同的appDomain传送。还可以让另一个SurrogateSelector对象维护一组序列化代理,这些序列化代理用于将版本1的类型转换成版本2的类型。