65.9K
CodeProject 正在变化。 阅读更多。
Home

通过序列化实现持久化

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.60/5 (7投票s)

2011年1月6日

CPOL

5分钟阅读

viewsIcon

30505

downloadIcon

370

本文从数据访问、可读性和运行时成本等方面对两种常见的序列化类型进行了比较。

摘要

持久化是指应用程序存储对象状态并在需要时恢复该状态的能力。本文从数据访问、可读性和运行时成本等方面对两种常见的序列化类型进行了比较。提供了一个使用BinaryFormatter并带有简单加密的即用型代码片段。

引言

第一次阅读 .NET 关于序列化的文档时,我感到非常惊讶。在 .NET 时代之前,处理配置文件是一件令人头疼的事情。您必须编写大量的代码来将数据流式传输到文件,然后再次解析长的 string 以找出要读回的正确数据。在玩弄序列化时,我希望创建一个完整的应用程序缓存,并像如今的 Windows 系统“休眠”功能一样恢复它。尽管现实总是与想象相去甚远,但 .NET 序列化在缓存应用程序的“一部分”——即数据对象——方面仍然非常有用。

.NET Framework 提供了两种类型的序列化:浅层序列化和深层序列化,分别由
System.Xml.Serialization 命名空间中的 XmlSerializer
System.Runtime.Serialization.Formatters.Binary 命名空间中的 BinaryFormatter 表示。
这两者之间的区别很明显:前者旨在以人类可读的 XML 格式保存和加载对象,后者提供紧凑的二进制编码,用于存储或网络流。 .NET Framework 还包含抽象的 FORMATTERS 类,可以用作自定义格式化器的基类。本文将重点介绍 XmlSerializerBinaryFormatter

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)

demo1.PNG

使内置类型可序列化的解决方法(测试用例 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 基础

附件包中的第二个项目与第一个项目类似,只是做了一些小的改动:

  1. 使用 BinaryFormatter 替换了 XmlSerializer
  2. 在每个类前面都添加了 “[Serializable]” 属性
  3. DerivedClass 中添加了一个内置图形类型 “Brush

对上述类执行了相同的测试。BinaryFormatter 的优点包括:

  • 对象中的所有 publicprivate 字段都可以被序列化(测试用例 11)
  • 不再需要声明默认构造函数(测试用例 12)。但最好还是与参数化构造函数一起生成一个默认构造函数。
  • 几乎所有内置类型都得到支持,但有一些例外,例如图形对象,它们没有定义 Serializable 属性。(测试用例 14)
  • Static 字段不可序列化,因为它不是对象引用(不是对象的一部分),如下图所示(测试用例 15)。

demo2.PNG

但是,如果您确实想让 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 的值是持久的。

demo5.PNG

  • 二进制操作比 string 操作快得多(测试用例 16)

demo3.PNG

  • Dictionary 类型也得到支持,但成本稍高(测试用例 17)。

demo4.PNG

基本上,您不必过多担心数据类型,只需将 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日:初始发布 
© . All rights reserved.