通过序列化实现持久化
本文从数据访问、可读性和运行时成本等方面对两种常见的序列化类型进行了比较。
摘要
持久化是指应用程序存储对象状态并在需要时恢复该状态的能力。本文从数据访问、可读性和运行时成本等方面对两种常见的序列化类型进行了比较。提供了一个使用BinaryFormatter
并带有简单加密的即用型代码片段。
引言
第一次阅读 .NET 关于序列化的文档时,我感到非常惊讶。在 .NET 时代之前,处理配置文件是一件令人头疼的事情。您必须编写大量的代码来将数据流式传输到文件,然后再次解析长的 string
以找出要读回的正确数据。在玩弄序列化时,我希望创建一个完整的应用程序缓存,并像如今的 Windows 系统“休眠”功能一样恢复它。尽管现实总是与想象相去甚远,但 .NET 序列化在缓存应用程序的“一部分”——即数据对象——方面仍然非常有用。
.NET Framework 提供了两种类型的序列化:浅层序列化和深层序列化,分别由System.Xml.Serialization
命名空间中的 XmlSerializer
和System.Runtime.Serialization.Formatters.Binary
命名空间中的 BinaryFormatter
表示。
这两者之间的区别很明显:前者旨在以人类可读的 XML 格式保存和加载对象,后者提供紧凑的二进制编码,用于存储或网络流。 .NET Framework 还包含抽象的 FORMATTERS
类,可以用作自定义格式化器的基类。本文将重点介绍 XmlSerializer
和 BinaryFormatter
。
XmlSerializer 基础
附件包中有三个项目。第一个项目 XMLSerializerSample
展示了 XmlSerializer
可以应用的一些典型场景。在 SampleClasses.cs 文件中,定义了三个示例类:
BuildinType
包含具有主类型的属性DerivedClass
使用内置引用类型,也展示了一个具有基类的类CollectionTypes
声明了几个不同的内置集合类型
Main
程序例程只是将每个类的实例依次序列化到一个文件并读回,外加一个数组对象来测试批量数据的性能。我在源代码和文章中都标记了测试用例编号。如果您愿意,可以自己执行测试。源代码中包含简单的指南,说明了软件测试文档 (STD) 的基本要素。
程序的输出如下:
test2.xml (Test Case 1):
<?xml version="1.0"?>
<DerivedClass xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<InstanceID>2</InstanceID>
<Number>300.900024</Number>
<Description>This is a test.</Description>
<TestState>DONE</TestState>
<TestTime>2010-12-08T02:23:50.265625+08:00</TestTime>
<StrFont>Times New Roman, 10pt</StrFont>
</DerivedClass>
XmlSerializer
支持:
- 所有主类型(测试用例 2)
- 派生类(测试用例 3)
- 简单集合类型,如数组、列表(测试用例 4)
- 仅
Public
数据成员(测试用例 5)
限制包括:
- 大多数内置引用类型不可序列化(测试用例 6)
Static
数据成员不会被序列化(测试用例 7)Private
字段无法保存(测试用例 5)- 必须有一个默认构造函数。通常,编译器会在没有显式构造函数的情况下生成一个。但有时我们可能会创建一个参数化构造函数,却忘记添加默认构造函数。那样的话,序列化就会被“意外”禁用。(测试用例 8)
String
操作非常昂贵,并且文本格式的存储量巨大(测试用例 9)
使内置类型可序列化的解决方法(测试用例 10)
Font thisFont = new Font("Times New Roman", 10F);
[XmlIgnore]
public Font ThisFont //Accessors for general calling.
{
get { return thisFont; }
set { thisFont = value; }
}
public string StrFont //Accessors for serialization.
{
get { return Utility.ObjectToString(thisFont); }
set { thisFont = (Font)Utility.ObjectFromString(typeof(Font), value); }
}
总的来说,XmlSerializer
最大的优点是输出的人类可读格式。如果您有一个相对简单的对象,并且需要直接修改数据,那么 XmlSerializer
是一个不错的选择。
BinaryFormatter 基础
附件包中的第二个项目与第一个项目类似,只是做了一些小的改动:
- 使用
BinaryFormatter
替换了XmlSerializer
- 在每个类前面都添加了 “
[Serializable]
” 属性 - 在
DerivedClass
中添加了一个内置图形类型 “Brush
”
对上述类执行了相同的测试。BinaryFormatter
的优点包括:
- 对象中的所有
public
和private
字段都可以被序列化(测试用例 11) - 不再需要声明默认构造函数(测试用例 12)。但最好还是与参数化构造函数一起生成一个默认构造函数。
- 几乎所有内置类型都得到支持,但有一些例外,例如图形对象,它们没有定义
Serializable
属性。(测试用例 14) Static
字段不可序列化,因为它不是对象引用(不是对象的一部分),如下图所示(测试用例 15)。
但是,如果您确实想让 static
成员可序列化,可以实现 ISerializable
接口来手动添加信息并恢复它(测试用例 16)。
[Serializable]
public class BuildinType: ISerializable
{
static int instanceCount = 0;
public BuildinType(SerializationInfo info, StreamingContext context)
{
BuildinType.instanceCount = info.GetInt32("instanceCount");
}
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.AddValue("instanceCount", instanceCount, typeof(int));
}
现在 instanceCount
的值是持久的。
- 二进制操作比
string
操作快得多(测试用例 16)
Dictionary
类型也得到支持,但成本稍高(测试用例 17)。
基本上,您不必过多担心数据类型,只需将 SerializableAttribute
添加到您的类中即可。然后,您就可以通过将对象保存在任何需要的地方来实现持久性。对于无法正确持久化的类型,您可以将 NonSerializedAttribute
添加到数据成员上,以便序列化器忽略它,或者实现 ISerializable
接口使其可序列化。
使用示例
从上述实验可以看出,优先选择 BinaryFormatter
而不是 XmlSerializer
是自然而然的。即使是配置文件,也建议通过用户界面来修改数据,而不是直接操作输出文件中的数据。附件包中的第三个项目提供了另外两个辅助函数,用于在不加密的情况下保存和加载数据。
public static void TSerialize(object theObject, string sFileName)
{
BinaryFormatter btFormatter = new BinaryFormatter();
FileStream theFileStream = new FileStream
(sFileName, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite);
btFormatter.Serialize(theFileStream, theObject);
theFileStream.Close();
}
public static object TDeSerialize(Type theType, string sFileName)
{
if (sFileName == null || sFileName == "" || !File.Exists(sFileName))
{
return null;
}
FileStream theFileStream = new FileStream
(sFileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
BinaryFormatter btFormatter = new BinaryFormatter();
object theObj = btFormatter.Deserialize(theFileStream);
theFileStream.Close();
return theObj;
}
以及使用简单加密和解密方法的函数。
public static void SerializeWithEncrypt(object theObject, string sFileName)
{
MemoryStream theMS = new MemoryStream();
BinaryFormatter btFormatter = new BinaryFormatter();
btFormatter.Serialize(theMS, theObject);
theMS.Seek(0, SeekOrigin.Begin);
byte[] temp = theMS.ToArray();
temp = Encrypt(temp);
//Output to a file.
FileStream theFileStream = new FileStream
(sFileName, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite);
BinaryWriter theBW = new BinaryWriter(theFileStream);
theBW.Write(temp, 0, temp.Length);
theBW.Close();
theFileStream.Close();
theMS.Dispose();
}
public static object DeSerializeWithDecrypt(string sFileName)
{
if (sFileName == null || sFileName == "" || !File.Exists(sFileName))
{
return null;
}
byte[] temp = File.ReadAllBytes(sFileName);
temp = Decrypt(temp);
MemoryStream theMS = new MemoryStream(temp);
BinaryFormatter btFormatter = new BinaryFormatter();
object theObj = btFormatter.Deserialize(theMS);
theMS.Dispose();
return theObj;
}
Configuration
类实现为单例模式。在首次调用创建单例实例时加载持久化数据。
[Serializable]
public sealed class Configuration
{
private static Configuration instance = null;
private Configuration()
{
}
public static Configuration Instance
{
get
{
if (instance == null)
{
instance = (Configuration)Utility.TDeSerialize("test.dat");
}
if (instance == null)
{
instance = new Configuration();
}
return instance;
}
}
…
…
以上所有代码均可在附件包中找到。
另一个附件应用程序 TCPaint
使用完全相同的代码来持久化窗体的大小和位置以及其他配置设置数据,例如 MRU(最近使用过的文件)。无限步的撤销和重做操作也使用此技术保存。用户可以始终回溯并修改他们的绘图,将其视为一系列单独的对象,而不是位图图像。
总之,正确使用序列化可以为您节省大量时间和精力。
历史
- 2011年1月6日:初始发布