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

BinaryFormatter 与手动序列化

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (12投票s)

2012 年 1 月 10 日

CPOL

6分钟阅读

viewsIcon

120397

downloadIcon

4330

比较使用 BinaryFormatter(标准的 .NET 类)进行序列化与手动逐字节序列化;在选择适合您的方法之前,一些优缺点。

引言

首先,让我们澄清“序列化”一词

在计算机科学中,在数据存储和传输的上下文中,序列化是将数据结构或对象状态转换为可以存储(例如,在文件或内存缓冲区中,或通过网络连接链路传输)并在同一或另一个计算机环境中稍后“恢复”的格式的过程。(维基百科

序列化是将对象的状态转换为可以持久化或传输的形式的过程。序列化的补充是反序列化,它将流转换为对象。(MSDN

序列化技术大致可分为两种:**人类可读**和**二进制**。 .NET Framework 提供了 BinaryFormatter 类用于二进制序列化,并提供了 XmlSerializer 类用于人类可读技术。

大多数来源建议,如果我们需要的序列化数据量较小,则使用 BinaryFormatter 而不是 XmlSerializer。我几乎从不使用 XmlSerializer - 它会对您的类带来一些限制。然而,BinaryFormatter 的使用相当频繁,我对此有一些看法。

背景

我在 .NET Framework 2.0 发布时开始使用 BinaryFormatter。当时还没有 WCF,二进制序列化被 Remoting 使用。我们开发了一个应用程序,其中应该有一个带有消息系统的特殊协议。我们有权选择消息编码方式。我们决定尝试 BinaryFormatter,它运行正常。一段时间后。当我们尝试在循环中发送一千条消息时 - 我们遇到了 TCP 零窗口大小的问题 - 这意味着接收方无法及时反序列化消息。

当我检查网络流量时,我发现每条消息的大小约为 1000 字节,而手动逐字节编码最多只需要 70 字节,这让我非常惊讶。

BinaryFormatter 和手动方法

在示例应用程序中,我们有两个“有用”的类 - Person 聚合 CreditCard,如图所示

PersonCreditCard.jpg

我们将序列化 Person 类的对象。要使用 BinaryFormatter 进行序列化,我们需要在 Person 类定义之前,以及在 Person 聚合的所有类之前,添加 Serializable 属性。然后,我们可以编写

BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(stream, toSerialize);

其中 streamtoSerialize 分别是 StreamPerson 类型的变量。

实现手动同步时,事情会变得有点复杂。我们必须提供一些方法供以后调用,而不是放置 Serializable 属性。在示例中,这样的方法是序列化类内部的静态成员 WriteToStreamReadFromStream。然后我们写

Person.WriteToStream(toSerialize, stream);

就是这样。

使用代码

在简要探讨了这两种技术后,让我们来检查测试代码。

static byte[] getBytesWithBinaryFormatter(Person toSerialize)
{ 
    MemoryStream stream = new MemoryStream();
    BinaryFormatter formatter = new BinaryFormatter();
    formatter.Serialize(stream, toSerialize);
    return stream.ToArray();
}

static byte[] getBytesWithManualWrite(Person toSerialize)
{
    MemoryStream stream = new MemoryStream();
    Person.WriteToStream(toSerialize, stream);
    return stream.ToArray();
}

static void testSize(Person toTest)
{
    byte[] fromBinaryFormatter = getBytesWithBinaryFormatter(toTest);
    Console.WriteLine("\tWith binary formatter: {0} bytes", 
                      fromBinaryFormatter.Length);
    byte[] fromManualWrite = getBytesWithManualWrite(toTest);
    Console.WriteLine("\tManual serializing: {0} bytes", fromManualWrite.Length);
}

static void testTime(Person toTest, int timesToTest)
{
    List<byte[]> binaryFormatterResultArrays = new List<byte[]>();
    List<byte[]> manualWritingResultArrays = new List<byte[]>();

    DateTime start = DateTime.Now;
    for (int i = 0; i < timesToTest; ++i )
        binaryFormatterResultArrays.Add(getBytesWithBinaryFormatter(toTest));
    TimeSpan binaryFormatterSerializingTime = DateTime.Now - start;

    Console.WriteLine("\tBinary formatter serializing: {0} seconds", 
                      binaryFormatterSerializingTime.TotalSeconds);

    start = DateTime.Now;
    for (int i = 0; i < timesToTest; i++)
        manualWritingResultArrays.Add(getBytesWithManualWrite(toTest) );
    TimeSpan manualSerializingTime = DateTime.Now - start;
    
    Console.WriteLine("\tManual serializing: {0} seconds", 
                      manualSerializingTime.TotalSeconds);
    BinaryFormatter formatter = new BinaryFormatter();
    start = DateTime.Now;
    foreach (var next in binaryFormatterResultArrays)
        formatter.Deserialize(new MemoryStream(next));
    TimeSpan binaryFormatterDeserializingTime = DateTime.Now - start;

    Console.WriteLine("\tBinaryFormatter desrializing: {0} seconds", 
                      binaryFormatterDeserializingTime.TotalSeconds );

    start = DateTime.Now;
    foreach (var next in manualWritingResultArrays)
        Person.ReadFromStream(new MemoryStream(next));
    TimeSpan manualDeserializingTime = DateTime.Now - start;

    Console.WriteLine("\tManual desrializing: {0} seconds", 
                      manualDeserializingTime.TotalSeconds );
}

static void addCards(Person where, int howMany)
{
    for (int i = 0; i < howMany; ++i)
    { 
        where.AddNewCard(new CreditCard(Guid.NewGuid().ToString(), 
                         new DateTime(2012, 12, 24 )));
    }
}

static void Main(string[] args)
{
    Person aPerson = new Person("Kate", new DateTime(1986, 2, 1), 
      new CreditCard[] { new CreditCard(Guid.NewGuid().ToString(), 
      new DateTime(2012, 12, 24 )) });
    Console.WriteLine("Size test: 1 card");
    testSize(aPerson);
    Console.WriteLine("Time test: 1 card, 100 000 times");
    testTime(aPerson, 100000);

    addCards(aPerson, 10000);
    Console.WriteLine("Size test: 10 000 cards");
    testSize(aPerson);

    Console.WriteLine("Time test: 10 000 cards, 100 times");
    testTime(aPerson, 100);

    Console.ReadKey();
}

首先,对包含一个 CreditCard 对象的 Person 进行大小和时间测试。然后对一个拥有 10,000 张信用卡的人进行相同的测试 :)。以下是我机器的输出

Size test: 1 card
        With binary formatter: 896 bytes
        Manual serializing: 62 bytes
Time test: 1 card, 100 000 times
        Binary formatter serializing: 4,24 seconds
        Manual serializing: 0,28 seconds
        BinaryFormatter desrializing: 4,57 seconds
        Manual desrializing: 0,169 seconds
Size test: 10 000 cards
        With binary formatter: 640899 bytes
        Manual serializing: 450062 bytes
Time test: 10 000 cards, 100 times
        Binary formatter serializing: 4,713 seconds
        Manual serializing: 0,527 seconds
        BinaryFormatter desrializing: 4,519 seconds
        Manual desrializing: 0,57 seconds

基于这些结果以及我们所掌握的知识,让我们比较一下这两种技术。

大小

使用 BinaryFormatter 序列化消息的成本很高,因为它包含元数据。手动序列化在此提供了实际的好处。在示例中,我们获得的字节数组结果缩小了近 14 倍。但是,当我们谈论序列化对象数组时 - 那么结果数组的大小差异就没有那么大了 - 在我们的示例中,手动同步的结果数组要小 1.4 倍。

速度

在速度方面,我们为单个对象和对象数组的序列化都获得了不错的优势。对于一个 Person 和一个 CreditCard 对象,我们获得了 4.24 / 0.28 = 15.14 倍的速度提升。并且手动方法对于 Person 对象内的 10,000 个 CreditCard 对象来说,速度快了近 10 倍。

速度测试通常需要提及机器的参数,一个显示一些依赖关系的表格 - 欢迎您自己测试并绘制表格。我不是试图给出精确的结果,而是呈现一个普遍的图景。

版本控制

好吧,协议会改变,三年后,您的初始消息格式可能与现在大不相同。字段可能会被删除和添加。因为我们在网络环境中考虑序列化,所以我们应该记住:所有对等方都应该对消息格式的变化做出良好的响应。

在手动序列化方法中,我们有两种选择:要么在格式更改时更新所有对等方,要么提供自己的方法来处理接收到其他版本消息的情况。

在 .NET 中,新字段可以标记为可选 - 它们可能存在于消息中 - 也可能不存在。当然,这并不能解决所有问题,并且新版本的宿主在接收消息时,应该处理可选字段的存在和缺失。

开发速度

手动序列化确实需要时间。用于开发和测试。如果您有版本控制逻辑 - 那么它将花费更多的时间。标准的 .NET 方法为您节省了所有这些时间。

技术

当您使用 TCP 绑定时,WCF 中会使用 BinaryFormatter。理论上,您可以提供自己的格式化程序,但我从未听说过任何好的实现。

结构复杂度影响

BinaryFormatter 能够序列化任何复杂度的对象。这意味着,即使您的系统中的对象形成了一个带有循环的图 - 它也会被正确序列化。同样,序列化(以及反序列化!)类层次结构中的叶类也不是问题。

手动序列化方面情况就没那么乐观了。拥有一个不聚合任何其他类的类是微不足道的情况。然后,当您有一个对象树时(Person 内部有 100 个 CreditCard 对象就是一个树)- 我们会遇到一些复杂性。拥有类层次结构会带来更多复杂性,而循环的独立性会让您的代码完全难以理解。我个人从未对可能包含循环的对象图使用过手动同步。

摘要

BinaryFormatter 的**优点**

  • 能够使用 WCF 或其他技术
  • 无需花费时间实现序列化机制
  • 存在版本控制支持
  • 可以处理任何结构复杂度的对象

手动序列化的**优点**

  • 可以达到最佳的大小复杂度
  • 可以达到最佳性能

关注点

手动序列化:叶类序列化

序列化,特别是反序列化代码,会涉及许多方面。

手动序列化:序列化代码放在哪里?

实际上,这是之前关注点的一个延续。在这个示例中,我将代码作为静态方法放在各自的类中。这种方法可以通过信息专家 GRASP 模式来解释。但是,由于没有类层次结构,示例项目中的一切都显得整洁。当出现类层次结构时,使用静态方法的方法看起来很糟糕。也许这一点可以在下一篇文章中涵盖。

Protobuffhttp://protobuf-net.googlecode.com。有趣的项目,非常辛苦的工作。我对他们的序列化器进行了相同的测试 - 并且获得了与手动方法相同的尺寸特性!然而,时间消耗比 BinaryFormatter 更糟糕 **(在 Debug 生成中 - 感谢 Sam Cragg 的评论)**。您可以 下载此代码 - 92.5 KB - 与示例项目相同,但还包含 Protobuff 检查,以便您自己查看。Release 生成的 Protobuff-net 测试 在此 - 86.6 KB。Release 生成显示出比 BinaryFormatter 好 3-5 倍的时间特性,但比手动方法的时间特性差。

为什么 BinaryFormatter 如此缓慢且占用空间?

历史

  • 2012 年 10 月 1 日 - 首次上传。
  • 2012 年 1 月 13 日 - 更新以反映反馈中的评论。
© . All rights reserved.