所谓温故而知新,现在重新学习一下序列化和反序列化,以求有更深一步的理解。
一、为什么要使用序列化?
最重要的两个原因是:将对象的状态保存在存储媒体中以便可以在以后重新创建出完全相同的副本;按值将对象从一个应用程序域发送至另一个应用程序域。例如,序列化可用于在 ASP.NET 中保存会话状态,以及将对象复制到 Windows 窗体的剪贴板中。它还可用于按值将对象从一个应用程序域远程传递至另一个应用程序域。本文简要介绍了 Microsoft .NET 中使用简介
序列化是指将对象实例的状态存储到存储媒体的过程。在此过程中,先将对象的公共字段和私有字段以及类的名称(包括类所在的程序集)转换为字节流,然后再把字节流写入数据流。在随后对对象进行反序列化时,将创建出与原对象完全相同的副本。
在面向对象的环境中实现序列化机制时,必须在易用性和灵活性之间进行一些权衡。只要您对此过程有足够的控制能力,就可以使该过程在很大程度上自动进行。例如,简单的二进制序列化不能满足需要,或者,由于特定原因需要确定类中那些字段需要序列化。以下各部分将探讨 .NET 框架提供的可靠的序列化机制,并着重介绍使您可以根据需要自定义序列化过程的一些重要功能。
二、持久存储
我们经常需要将对象的字段值保存到磁盘中,并在以后检索此数据。尽管不使用序列化也能完成这项工作,但这种方法通常很繁琐而且容易出错,并且在需要跟踪对象的层次结构时,会变得越来越复杂。可以想象一下编写包含大量对象的大型业务应用程序的情形,程序员不得不为每一个对象编写代码,以便将字段和属性保存至磁盘以及从磁盘还原这些字段和属性。序列化提供了轻松实现这个目标的快捷方法。
公共语言运行时 (CLR) 管理对象在内存中的分布,.NET 框架则通过使用反射提供自动的序列化机制。对象序列化后,类的名称、程序集以及类实例的所有数据成员均被写入存储媒体中。对象通常用成员变量来存储对其他实例的引用。类序列化后,序列化引擎将跟踪所有已序列化的引用对象,以确保同一对象不被序列化多次。.NET 框架所提供的序列化体系结构可以自动正确处理对象图表和循环引用。对对象图表的唯一要求是,由正在进行序列化的对象所引用的所有对象都必须标记为 Serializable。否则,当序列化程序试图序列化未标记的对象时将会出现异常。
当反序列化已序列化的类时,将重新创建该类,并自动还原所有数据成员的值。
三、按值封送
对象仅在创建对象的应用程序域中有效。除非对象是从 MarshalByRefObject 派生得到或标记为 Serializable,否则,任何将对象作为参数传递或将其作为结果返回的尝试都将失败。如果对象标记为 Serializable,则该对象将被自动序列化,并从一个应用程序域传输至另一个应用程序域,然后进行反序列化,从而在第二个应用程序域中产生出该对象的一个精确副本。此过程通常称为按值封送。
如果对象是从 MarshalByRefObject 派生得到,则从一个应用程序域传递至另一个应用程序域的是对象引用,而不是对象本身。也可以将从 MarshalByRefObject 派生得到的对象标记为 Serializable。远程使用此对象时,负责进行序列化并已预先配置为 SurrogateSelector 的格式化程序将控制序列化过程,并用一个代理替换所有从 MarshalByRefObject 派生得到的对象。如果没有预先配置为 SurrogateSelector,序列化体系结构将遵从下面的标准序列化规则。
四、序列化过程的步骤
在格式化程序上调用 Serialize 方法时,对象序列化按照以下规则进行:
1.检查格式化程序是否有代理选取器。如果有,检查代理选取器是否处理指定类型的对象。如果选取器处理此对象类型,将在代理选取器上调用 ISerializable.GetObjectData。
2.如果没有代理选取器或有却不处理此类型,将检查是否使用 Serializable 属性对对象进行标记。如果未标记,将会引发 SerializationException。
3.如果对象已被正确标记,将检查对象是否实现了 ISerializable。如果已实现,将在对象上调用 GetObjectData。
4.如果对象未实现 Serializable,将使用默认的序列化策略,对所有未标记为 NonSerialized 的字段都进行序列化。
五、版本控制
.NET 框架支持版本控制和并排执行,并且,如果类的接口保持一致,所有类均可跨版本工作。由于序列化涉及的是成员变量而非接口,所以,在向要跨版本序列化的类中添加成员变量,或从中删除变量时,应谨慎行事。特别是对于未实现 ISerializable 的类更应如此。若当前版本的状态发生了任何变化(例如添加成员变量、更改变量类型或更改变量名称),都意味着如果同一类型的现有对象是使用早期版本进行序列化的,则无法成功对它们进行反序列化。
如果对象的状态需要在不同版本间发生改变,类的作者可以有两种选择:
1.实现 ISerializable。这使您可以精确地控制序列化和反序列化过程,在反序列化过程中正确地添加和解释未来状态。
2.使用 NonSerialized 属性标记不重要的成员变量。仅当预计类在不同版本间的变化较小时,才可使用这个选项。例如,把一个新变量添加至类的较高版本后,可以将该变量标记为 NonSerialized,以确保该类与早期版本保持兼容。
六、序列化规则
由于类编译后便无法序列化,所以在设计新类时应考虑序列化。需要考虑的问题有:是否必须跨应用程序域来发送此类?是否要远程使用此类?用户将如何使用此类?也许他们会从我的类中派生出一个需要序列化的新类。只要有这种可能性,就应将类标记为可序列化。除下列情况以外,最好将所有类都标记为可序列化:
1.所有的类都永远也不会跨越应用程序域。如果某个类不要求序列化但需要跨越应用程序域,请从 MarshalByRefObject 派生此类。
2.类存储仅适用于其当前实例的特殊指针。例如,如果某个类包含非受控的内存或文件句柄,请确保将这些字段标记为 NonSerialized 或根本不序列化此类。
3.某些数据成员包含敏感信息。在这种情况下,建议实现 ISerializable 并仅序列化所要求的字段。
七、序列化方法
.Net中的序列化方法有三种:XML 序列化、SOAP 序列化和二进制序列化。若是序列化到文件的话,前两者生成的是 XML 文件,二进制序列化生成二进制文件。
跟序列化相关的两个类型:
SerializableAttribute:指示一个类是可以序列化的。
ISerializable:使对象可以自己控制其序列化和反序列化的过程
列表比较三种序列化方法。
| XML | SOAP | 二进制 |
序列化器类 | XmlSerializer | SoapFormatter | BinaryFormatter |
SerializableAttribute 标记 | 不需要 | 需要 | |
ISerializable 接口 | 不需要实现,实现了也不起作用。 | 可以不实现,但实现了就起作用。 | |
无参构造函数 | 必须有,系统提供的缺省无参构造函数也算。 | 不需要,因为反序列化时不调用构造函数。 | |
被序列化的数据成员 | 必须有,系统提供的缺省无参构造函数也算。 | 不需要,因为反序列化时不调用构造函数。 | |
被序列化的数据成员 | 公共属性和字段 | 所有 | |
产生文件大小 | 大 | 大 | 小 |
1. XML序列化
XML序列化的优点是使用简单,也颇具灵活性,比如可以控制数据在 XML 文件中是作为 Element 还是作为 Attribute ,以及显示的名称等。但XML 序列化不转换方法、索引器、私有字段或只读属性。使用 XmLSerializer 类可将下列项序列化:1.公共类的公共读/写属性和字段。 2.实现 ICollection 或 IEnumerable 的类.
先看一下代码,看如何实现xml序列化:
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Xml;
using System.Xml.Serialization;
using System.Runtime.Serialization;
using System.IO;
///<summary>
/// Company 的摘要说明
///</summary>
[XmlRoot(Namespace = "http://www.cpdyj.com", ElementName = "BitTime")]//ElementName指定根结点名称
public class Company
{
public Company()
{
//
// TODO: 在此处添加构造函数逻辑
//
}
privatestring _companyName;
[XmlElement(ElementName = "公司名称")]//指定元素名称
publicstring CompanyName
{
get { return _companyName; }
set { _companyName = value; }
}
privatestring _companyAddress;
[XmlElement(ElementName = "公司地址")]
publicstring CompanyAddress
{
get { return _companyAddress; }
set { _companyAddress = value; }
}
privatestring _companyPostCode = "100085";
[XmlElement(ElementName = "邮政编码")]
publicstring CompanyPostCode
{
get { return _companyPostCode; }
set { _companyPostCode = value; }
}
privatestring _companyBoss;
[XmlElement(ElementName = "法人代表")]
///<summmary>
///法人代表
///</summary>
publicstring CompanyBoss
{
get { return _companyBoss; }
set { _companyBoss = value; }
}
}
//xml反序列化
protected void XmlDeSerialize(object sender, EventArgs e)
{
XmlSerializer formatter = new XmlSerializer(typeof(Company));
Stream stream = new FileStream(Server.MapPath("company.xml"), FileMode.Open, FileAccess.Read, FileShare.Read);
Company obj = (Company)formatter.Deserialize(stream);
stream.Close();
}
//xml序列化
protected void XmlSerialize(object sender, EventArgs e)
{
Company company = new Company();
company.CompanyName = "北京";
company.CompanyBoss = "海淀";
company.CompanyAddress = "北京海淀区苏州街北段";
XmlSerializer xml = new XmlSerializer(typeof(Company));
Stream stream = new FileStream(Server.MapPath("company.xml"), FileMode.Create, FileAccess.Write, FileShare.None);
xml.Serialize(stream, company);
stream.Close();
}
XML 序列化对一般的应用是足以应付的,但当序列化循环引用的对象,即有多个引用指向同一个对象实体时,XML 序列化机制将在每个引用的地方都创建一个对象副本。这除了会导致数据存储上的冗余外,更严重的是使一个对象在反序列化后变成了毫无关系的多个对象,即 XML 反序列化后可能得到错误的对象关系图表。比如下图所示的简单例子:
对应三种类型分别有三个对象 cObject 及其成员 _aObject、 _bObject,_aObject 由 cObject 构造,_bObject 中存的是对 _aObject 的引用,即 cObject 的成员 _aObject 和 _bObject 的成员 _aObject 是同一个东西。则 ClassC 类型的对象 cObject 经 XML 序列化的结果是:
<ClassC xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Name>ccc</Name>
<AObject>
<ID>37</ID>
<Name>aaa</Name>
</AObject>
<BObject>
<Name>bbb</Name>
<AObject>
<ID>37</ID>
<Name>aaa</Name>
</AObject>
</BObject>
</ClassC>
从这个结果反序列化后得到的新的 cObject,其成员 _aObject 跟 _bObject 中的 _aObject 可就是两个对象了。要解决这个问题,我能想到的就是给对象加上 GUID 属性,在反序列化后根据 GUID 属性重新设置引用,不知还有没有其它办法。
2. SOAP 和二进制序列化
SOAP 和二进制序列化的优点是可以精确地控制序列化及反序列化的过程,并可以序列化对象的非公共成员。所以对复杂对象的序列化,我们应该在实现 ISerializable 接口后,用 SOAP 或 二进制的方式保存数据。
先看一下代码,看如何实现SOAP 和二进制序列化:
using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Runtime.Serialization;
using System.Security.Permissions;
///<summary>
/// TestCustomSerialize 的摘要说明
///</summary>
[Serializable]
public class TestCustomSerialize : ISerializable
{
public TestCustomSerialize()
{
//
// TODO: 在此处添加构造函数逻辑
//
}
public int n1;
public int n2;
public string str;
public DateTime date;
public TestCustomSerialize(SerializationInfo info, StreamingContext context)
{
n1=info.GetInt32("i");
n2=info.GetInt32("j");
str=info.GetString("k");
date = info.GetDateTime("dateTime").ToLocalTime();
}
#region ISerializable 成员
[SecurityPermissionAttribute(SecurityAction.Demand,SerializationFormatter=true)]
public virtual void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.AddValue("i",n1);
info.AddValue("j",n2);
info.AddValue("k",str);
info.AddValue("dateTime", date);
}
#endregion
}
如果不需要特殊处理,可以不继承ISerializable接口,并不实现GetObjectData方法。如果在序列化时对成员进行行加密或特殊处理,或者对某些成员序列化,某些成员不序列化,则需要继承ISerializable接口,并实现GetObjectData方法,这就是自定义序列化。
二进制序列化和反序列化
//二进制序列化
protected void BinarySerialize(object sender, EventArgs e)
{
TestCustomSerialize obj = new TestCustomSerialize();
obj.n1 = 1;
obj.n2 = 24;
obj.str = "Some String";
IFormatter formatter = new BinaryFormatter();
Stream stream = new FileStream(@"D:/Bit/Log4net/formatter.bin", FileMode.Create, FileAccess.Write, FileShare.None);
formatter.Serialize(stream, obj);
stream.Close();
}
//二进制反序列化
protected void BinaryDeSerialize(object sender, EventArgs e)
{
IFormatter formatter = new BinaryFormatter();
Stream stream = new FileStream(@"D:/Bit/Log4net/formatter.bin", FileMode.Open, FileAccess.Read, FileShare.Read);
TestCustomSerialize obj = (TestCustomSerialize)formatter.Deserialize(stream);
stream.Close();
}
Soap序列化和反序列化
//soap序列化
protected void SoapSerialize(object sender, EventArgs e)
{
//soap序列化与二进制一样只是生成的是xml文件而不是二进制代码
//使用soap序列化时必须添加System.Runtime.Serialization.Formatters.Soap的引用,然后引用该命名空间
TestCustomSerialize obj = new TestCustomSerialize();
obj.n1 = 1;
obj.n2 = 24;
obj.str = "Some String";
IFormatter formatter = new SoapFormatter();
Stream stream = new FileStream(@"D:/Bit/Log4net/formatter.bin", FileMode.Create, FileAccess.Write, FileShare.None);
formatter.Serialize(stream, obj);
stream.Close();
}
//soap反序列化
protected void SoapDeSerialize(object sender, EventArgs e)
{
IFormatter formatter = new SoapFormatter();
Stream stream = new FileStream(@"D:/Bit/Log4net/formatter.bin", FileMode.Open, FileAccess.Read, FileShare.Read);
TestCustomSerialize obj = (TestCustomSerialize)formatter.Deserialize(stream);
stream.Close();
}
还是上面的例子,如果用 SOAP 序列化 cObject 对象,结果是:
<SOAP-ENV:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:clr="http://schemas.microsoft.com/soap/encoding/clr/1.0" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<SOAP-ENV:Body>
<a1:ClassC id="ref-1" xmlns:a1="http://schemas.microsoft.com/clr/nsassem/SerializeTest.CrossReference/SerializeTest%2C%20Version%3D1.0.0.0%2C%20Culture%3Dneutral%2C%20PublicKeyToken%3Dnull">
<ClassCNameName id="ref-3">ccc</ClassCNameName>
<ClassCAObjectAObject href="#ref-4"/>
<ClassCBObjectBObject href="#ref-5"/>
</a1:ClassC>
<a1:ClassA id="ref-4" xmlns:a1="http://schemas.microsoft.com/clr/nsassem/SerializeTest.CrossReference/SerializeTest%2C%20Version%3D1.0.0.0%2C%20Culture%3Dneutral%2C%20PublicKeyToken%3Dnull">
<ClassANameName id="ref-6">aaa</ClassANameName>
<ClassAIDID>37</ClassAIDID>
</a1:ClassA>
<a1:ClassB id="ref-5" xmlns:a1="http://schemas.microsoft.com/clr/nsassem/SerializeTest.CrossReference/SerializeTest%2C%20Version%3D1.0.0.0%2C%20Culture%3Dneutral%2C%20PublicKeyToken%3Dnull">
<ClassBNameName id="ref-7">bbb</ClassBNameName>
<ClassBAObjectAObject href="#ref-4"/>
</a1:ClassB>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
很明显,里面存的是对象引用,这是一个精确副本,反序列化后毫无问题。
八、序列化要注意的问题
当你通过继承一个现有的类来定义你需要被序列化的类,如果这个父类实现了ISerializable接口,如果定义不当,就会出现反序列化的问题。而且这个我们可能经常都不注意。
例如:自定义一个字典类型MyDictionary,其Key和Value的类型分别为String和Object。最简单的方式莫过于直接继承Dictionary<string, object>。为了让MyDictionary对象可序列化,我们在类型上面应用了SerializableAttribute特性。MyDictionary定义如下:
[Serializable]
public class MyDictionary : Dictionary<string, object>
{
}
然后通过下面的代码对MyDictionary对象进行序列化和反序列化
static void Main(string[] args)
{
var dictionary = new MyDictionary();
dictionary.Add("001", "Foo");
dictionary.Add("002", "Bar");
dictionary.Add("003", "Baz");
using (MemoryStream stream = new MemoryStream())
{
var formatter = new BinaryFormatter();
formatter.Serialize(stream, dictionary);
stream.Seek(0, 0);
dictionary = (MyDictionary)formatter.Deserialize(stream);
}
foreach (var item in dictionary)
{
Console.WriteLine("{0}: {1}", item.Key, item.Value);
}
}
现在我们运行这段代码,在进行但序列化的时候出现如下SerialiationException的异常,错误消息提示找不到构造函数。
我们可以看到具体的错误消息:“The constructor to deserialize an object of type 'DeserializationIssue.MyDictionary' was not found.”,对于这个消息,我们第一个反应是在反序列化的时候找不到默认(无参)的构造函数。但是再看MyDictionary的定义,我们不曾定义任何构造函数,意味着它具有一个默认(无参)构造函数。
实际上,这里并不是找不到默认(无参)构造函数,而是找不到一个具有特殊参数列表的构造函数。该构造函数接收两个参数,类型分别是:SerializationInfo和StreamingContext。所以我们的解决方案很简单,就是加上这么一个构造函数。为此我们从新定义MyDictionary。
[Serializable]
public class MyDictionary : Dictionary<string, object>
{
public MyDictionary() { }
protected MyDictionary(SerializationInfo info, StreamingContext context) : base(info, context) { }
}
从新运行我们的程序,你就会得到想要的输出结果。
如果一个类型实现了ISerializable接口(Dictionary<TKey, TValue>就实现了这个接口),你就应该定义如上一个构造函数。这算是一个约定。