一,运行时序列化的作用
序列化(Serialization)是将一个对象转换成一个字节流的过程。反序列化(Deserialization)是将一个字节流转换回一个对象的过程。在对象和字节流之间的转化是非常有用的机制。下面是一些例子。
●应用程序的状态可以轻松保存到一个磁盘或数据库中,并在应用程序下次运行时恢复。Asp.net就是利用序列化和反序列化来保存和恢复会话状态的。
●一组对象可轻松复制到剪贴板,在粘贴回同一个或另一个应用程序。事实上,Windows窗体和WPF就是利用了这个功能。
●一组对象可以克隆放到一边作为“备份”;与此同时,用户操纵一组“主”对象。
●一组对象可轻松地通过网络发给另一台机器上运行的进程。Microsoft .Net Framework 的Remoting(运程处理)架构会对按值封送(marshaled by value)的对象进行序列化和反序列化。这个技术还可用于跨越AppDomain边界发送对象。
除了上述应用,一旦将对象序列化成内存中的一个字节流,就可以用一些更有用的方式来处理数据,比如加密和压缩数据等。
二,序列化/反序列化快速入门
先看一个简单的例子:
private void QuickStartSerialization() { //创建一个对象图,以便把它序列化到流中 var objectGraph = new List<string> { "Jeff", "Jim", "Jom" }; Stream stream = SerializeToMemory(objectGraph); //为了演示,将一切重置 stream.Position = 0; objectGraph = null; //反序列化,证明它能工作 objectGraph = (List<string>)DeserializFromMemory(stream); foreach (var s in objectGraph) { Console.WriteLine(s); } } private Stream SerializeToMemory(object graph) { //构造一个流来容纳序列化的对象 MemoryStream stream = new MemoryStream(); //构造一个序列化格式化器,让它负责所有复杂的工作 BinaryFormatter formatter = new BinaryFormatter(); //告诉格式化器序列化对象到一个流中 formatter.Serialize(stream, graph); //将序列化好的对象返回给调用者 return stream; } private object DeserializFromMemory(Stream stream) { //构造一个序列化格式化器,让它负责所有复杂的工作 BinaryFormatter formatter = new BinaryFormatter(); //告诉格式化器从流中反序列化对象 return formatter.Deserialize(stream); }
代码注释部分已经写得很清楚了,需要注意的是,Serialize方法的第一个参数是一个流对象的引用,他表示对象序列化后应该放到哪里。第二个参数是一个对象的引用,这个对象可以是任何东西,如Int32,String,Exception,List<String>,Dictinary<string,Datetime>等等。格式化器参考对象类型的元数据,从而了解如何序列化完整的对象图。序列化时,Serialize方法利用反射来查看每个对象的类型中都有哪些字段。在这些字段中,任何一个引用了其他对象,格式化器的Serialize方法就知道那些对象也要序列化。格式化器非常智能,如果对象图中的两个对象相互引用,格式化器会检测到这一点,确保每个对象只被序列化一次,避免进入无限循环。
Deserialize方法会检查流的内容,构造流中所有对象的实例,并初始化所有这些对象的字段。通常要将Deserialize返回的对象引用转换成应用程序期待的类型。
序列化时的注意事项:
●首先,你必须保证代码为序列化和反序列化使用相同的格式化器。例如:不要用SoapFormatter序列化一个对象,再用BinaryFormatter反序列化。如果Deserialize发现自己解释不了一个流中的内容,就会抛出一个System.Runtime.Serialization.SerializationException异常。
●其次,可以将多个对象序列化到一个流中,这是一个很有用的操作。例如,假定有下面两个类:
[Serializable] internal sealed class Customer { } [Serializable] internal sealed class Order { }
然后在应用程序主要类中,定义了以下字段:
private List<Customer> m_customers = new List<Customer>(); private List<Order> m_pendingOrders = new List<Order>(); private List<Order> m_processedOrders = new List<Order>();
下面可以用如下方法将应用程序的状态序列化到单个流中:
private void SaveApplicatonState(Stream stream) { BinaryFormatter formatter = new BinaryFormatter(); //序列化应用程序的完整状态 formatter.Serialize(stream, m_customers); formatter.Serialize(stream, m_pendingOrders); formatter.Serialize(stream, m_processedOrders); }
然后用下面的方法重建应用程序的状态:
private void RestoreApplicationState(Stream stream) { BinaryFormatter formatter = new BinaryFormatter(); //反序列化应用程序的完整状态(注意:和序列化的顺序一样) stream.Position = 0; m_customers = (List<Customer>)formatter.Deserialize(stream); m_pendingOrders = (List<Order>)formatter.Deserialize(stream); m_pendingOrders = (List<Order>)formatter.Deserialize(stream); }
●第三也是最后一点注意事项与程序集有关。序列化一个对象时,类型的全名和程序集的名称会被写入流。默认情况下,BinaryFormatter会输出程序集的完整标识,包括程序集的文件名,版本号,语言文化以及公钥信息。反序列化时,格式化器首先获得程序集的标识信息,并通过Assembly的Load方法加载程序集,确保程序集在正在执行的AppDomain中。程序集加载好后,格式化器在程序集中查找与要反序列化的对象匹配的一个类型。如果程序集不包含一个匹配的类型,就抛出一个异常,不再对更多的对象进行序列化。如果找到一个匹配的类型,就创建类型的一个实例,并以流中包含的值对字段进行初始化。如果类型中的字段与流中读取的字段名不完全匹配,就抛出一个SerializtionException异常,不再对更多的对象进行序列化。
重要提示:有的可扩展应用程序使用了Assembly.LoadFrom加载一个程序集,然后根据加载的程序集中的类型来构造对象。这些对象可以毫无问题的序列化到一个流中。然而,在反序列化是,格式化器会通过调用Assembly的Load方法(而非LoadFrom)来尝试加载程序集。在大多数情况下,CLR都无法定位程序集文件,将会抛出SerializatonException异常。许多开发人员为此感到不解。要解决这个问题,在调用格式化器的Deserialize方法之前,可以向System.AppDomain的AssemblyResolve事件注册一个System.ResoveEventHandler类型的委托方法,在这个方法中加载需要的程序集。在Deserialize方法返回后,马上注销这个委托方法。
FCL提供了2个格式化器,BinaryFormatter和SoapFormatter,从.Net 3.5起,SoapFormatter类已被废弃,如果要生成XML的序列化可以使用XmlSerializer和DataContractSerializer类。
三,使类型可序列化
设计一个类时,默认情况是不允许序列化的,要使类型可序列化,需要向类型应用一个名为System.SerializationAttribute特性。如:
[Serializable] internal sealed class Customer { }
注意:序列化一个对象时,有的对象也许能序列化,有的也许不能。考虑到性能,在序列化前,格式化器不会验证所有对象都能序列化。所以,序列化一个对象图时,在抛出SerializationException之前,完全有可能已经有一部分对象序列化到流中。如果发生这种情况,流中就包含已损坏的数据。如果你认为有些对象不可序列化,那么写的代码就应该能从这种情况中恢复。一个方案是,先将对象序列化到MemoryStream中,如果对象序列化成功,再将其复制到真正希望的目标流(比如文件或网络)。
SerializationAttribute特性能够应用于引用类型(class),值类型(struct),枚举(enum)和委托(delegate)类型。注意,枚举和委托总是可序列化的,不必显示指定这个特性。除此之外,SerializationAttribute不会被派生类型继承。下面的类型是不可序列化的:
[Serializable] internal sealed class Person { } internal sealed class Employee : Person { }
为解决这个问题,只需将SerializationAttribute应用于Employee类型。
[Serializable] internal sealed class Person { } [Serializable] internal sealed class Employee : Person { }
上述问题很容易修正,但反之则不然。如果一个基类没有应用SerializationAttribute特性,那么很难想象如何从它派生出一个可序列化的类型。这样的设计是有原因的,如果基类型不允许它的实例序列化,它的子类就不能序列化,因为基对象是派生对象的一部分。这正是System.Object已经很体贴的应用了SerializationAttribute的原因。
四,控制序列化和反序列化
将SerializationAttribute这个attribute应用于一个类型时,所有的实例字段(public,private,protected等)都会被序列化(在标记了[Serialization]特性的类型中,不要使用C#的“自动实现属性”来定义属性。这是因为字段名是由编译器自动生成的,而每次重新编译,生成的名称都不同)。然而,类型可能定义了一些不应序列化的实例字段,一般情况,有以下两个原因:
●字段含有反序列化后变得无效的信息。例如,假定一个对象包含一个Windows内核对象(如文件,进程,线程,信号量等)的句柄,在反序列化到另一进程或机器之后,就会失去意义。因为Windows内核对象是与进程相关的值。
●字段含有很容易计算的值。在这种情况下,要选出那些无需序列化的字段,减少要传输的数据,从而增强应用程序的性能。
下面的例子使用System.NonSerializedAttribute来指明哪些类型的字段不应序列化。
[Serializable] internal class Circle { private Double m_radius; [NonSerialized] private double m_area; public Circle(Double radius) { m_radius = radius; m_area = Math.PI * radius * radius; } }
上述代码保证了m_area字段不会被序列化,因为它应用了NonSerializedAttribute。注意这个特性只能应用于字段,而且会被派生类型继承。当然,可以向一个对象的多个字段应用这个特性。假定我们的代码像下面这样构造了一个Circle的实例:
Circle circle = new Circle(10);
在内部m_area被设置成了314.159的值。这个对象在序列化时,只有m_radius字段的值(10)才会写入流。这是我们希望的,但当反序列化成一个Circle对象时,就会遇到一个问题。反序列化对象的m_radius字段会被设置为10,但m_area字段会被初始化为0—而不是314.159。下面的代码演示如何修正这个问题。
[Serializable] internal class Circle { private Double m_radius; [NonSerialized] private double m_area; public Circle(Double radius) { m_radius = radius; m_area = Math.PI * radius * radius; } [OnDeserialized] private void OnDeserialized(StreamingContext context) { m_area = Math.PI * m_radius * m_radius; } }
在修改后的版本中,包含了一个应用了System.Runtime.Serialization.OnDeserializedAttribute特性的方法。每次反序列化类型的一个实例,格式化器都会检查是否有一个应用了该特性的方法。如果是,则调用该方法。调用这个方法时,所有可序列化的字段都会被正确设置。在方法中,可能需要访问这些字段来执行一些额外的操作,从而确保对象的完全序列化。上述修改后,在OnDeserialized方法中我们使用了m_radius来计算m_area的值,这样一来,m_area就有了我们希望的值(314.159)。
除了使用OnDeserializedAttribute,System.Runtime.Serialization命名空间还定义了OnDeserializingAttribute,OnSerializedAttribute和OnSerializingdAttribute特性。可将它们应用于类型中定义的方法,对序列化和反序列化进行更多的控制。
[Serializable] public class MyType { Int32 x, y; [NonSerialized] Int32 sum; public MyType(Int32 x, Int32 y) { this.x = x; this.y = y; this.sum = x + y; } [OnDeserializing] private void OnDeserializing(StreamingContext context) { //示例:在这个类型的新版本中,为字段设置默认值 } [OnDeserialized] private void OnDeserialized(StreamingContext context) { //示例:根据字段值初始化瞬时状态(比如sum的值) sum = x + y; } [OnSerializing] private void OnSerializing(StreamingContext context) { //示例:在序列化前,修改任何需要修改的状态 } [OnSerialized] private void OnSerialized(StreamingContext context) { //示例:在序列化后,恢复任何需要恢复的状态 } }
如果序列化一个类型的实例,在类型中添加了一个新的字段,然后试图反序列化不包含新字段的类型的对象,格式化器会抛出SerializationException异常,这非常不利于版本控制,因为我们经常需要在类型的一个新版本中添加新字段。幸好,这时可以利用OptionalFieldAttribute的帮助。类型中新增的每个字段都要应用OptionalFieldAttribute特性。然后,当格式化器看到该attribute应用于一个字段时,就不会因为流中的数据不包含这个字段而抛出SerializationException异常。
未完待续,下接《CLR via C#》笔记——运行时序列化(2)