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

原始序列化器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.81/5 (31投票s)

2006 年 1 月 8 日

CPOL

14分钟阅读

viewsIcon

129955

downloadIcon

1498

用此类替换 BinaryFormatter,实现紧凑(可空)值类型的序列化。

引言

虽然 .NET 提供了 BinaryReaderBinaryWriter,但这些类不足以处理结构体和可空值类型。相反,BinaryFormatter 是一个笨拙且臃肿的解决方案。我们需要的是一个能够生成紧凑的序列化数据流,同时支持可空数据值的东西,这包括经典的 C# 1.0 意义上的(装箱的值类型)和 C# 2.0 意义上的可空值类型。此外,nullDBNull.Value 之间的整个问题,C# 2.0 中的可空类型仍然没有解决

DateTime? dt = null;
dt = DBNull.Value; // compiler error!

需要处理(这意味着,序列化器需要保留装箱值类型是 null 还是 DBNull.Value)。

因此,原生序列化器/反序列化器就是这样工作的。当您将(可能可空的)值类型序列化为已知格式,并以相同格式反序列化这些值时,它是 BinaryFormatter 的替代品。

问题:Binary Formatter

BinaryFormatter 是一个非常低效的数据传输工具。它会创建一个大型“二进制”文件,并消耗大量内存,因为它不是流,并且可能导致应用程序崩溃。例如,一个典型的用法是将 DataTable 的内容打包

DataTable dt=LoadDataTable();
BinaryFormatter bf=new BinaryFormatter();
FileStream fs=new FileStream(filename, FileMode.Create);
bf.Serialize(fs, dt);
fs.Close();
  • 我尝试使用一个包含大约 200,000 条记录的表进行了测试,BinaryFormatter 因“内存不足”异常而崩溃。
  • 我用一个较小的表进行了测试,发现生成的二进制文件比估计的数据大小大 10 倍。
  • 在格式化过程中,它会消耗大量内存,使得该类在实际应用中难以使用,因为您不知道系统可能拥有什么样的物理内存。
  • 尽管 BinaryFormatter 接受输出流,但它显然不会在流关闭之前流式传输数据。

这些问题令人担忧,因此我决定寻找一个更精简的实现,并且一个不会因崩溃和消耗大量内存而受到影响的实现。

原生序列化器概述

有些文章比其他文章更难理解如何开始。这是一篇我犹豫了很久的文章。在最初的版本中,我花了大约一半的文章来谈论我为什么编写一个原生序列化类。最终,我决定讨论得太多了。然后在撰写文章时,我意识到我没有对称地处理代码的读写部分——写入器使用字典查找写入方法,而读取器则实现了一个 switch 语句。嗯。我还意识到实现会将值类型装箱以便写入,并在读取时要求调用者进行拆箱。此功能对于序列化 DataTable 是必需的,但如果您序列化已知类型,它会降低性能。考虑到这些类的主要目的(至少对我来说)是高效地序列化/反序列化 DataTable,我考虑保留此实现决定,但后来我认为这不一定适合其他人,所以我决定添加必要的方法来避免装箱/拆箱。最后,我意识到我需要探讨和理解 C# 2.0 的可空类型以及它们应该如何被支持。

总而言之,我认为一个描述各种代码路径的图(从绿色的框开始)将有助于读者理解正在发生的事情。

您可能会问自己,为什么不直接暴露 BinaryReader/Writer,以便在不需要可空支持时调用者可以使用适当的 Read/Write 方法?这个问题有些道理,因为当前的实现引入了一个可能被认为是不必要的函数调用。然而,封装的要点是允许接口(在本例中是 RawSerializer 类)发生变化而不影响调用者。如果将来我想使用 BinaryReader/Writer 以外的任何流,或者向读写方法添加额外功能,我可以安全地这样做,并且知道 BinaryReader/Writer 的封装没有被破坏。

所以,最终结果是一个更好、更完整的实现。我必须说,撰写一篇描述自己代码的文章是一种非常强大的代码审查技术!

原生序列化器

以下描述了原生序列化器通常可以处理的内容,以及在使用它时应注意的警告。

值类型、结构体和可空值

这些类将本机值类型(包括兼容的结构体——由本机类型组成且封送器可以确定其大小的结构体)直接序列化和反序列化为二进制值。RawSerializer 类及其补充 RawDeserializer 本身不是流,但它们封装了 BinaryWriterBinaryReader 类,这些类是流,因此允许原生序列化器在流上下文中工作。

由于序列化器仅支持值类型,因此它不是像 BinaryFormatter 这样的通用序列化引擎。但是,在您只序列化值类型的情况下,这类类将生成更高效的输出,因为它只写入原始二进制数据。

该类支持 DateTime?Guid? 可空值类型的直接序列化。其他可空结构将通过装箱机制进行,并要求您显式使用 SerializeNullable(...) 方法。对于反序列化,您必须显式指定适当的反序列化方法,例如 int? DeserializeNInt() 而不是 int DeserializeInt()。其他可空结构需要使用 object DeserializeNullable(Type t) 方法并显式拆箱返回值。

版本信息

BinaryFormatter 不同,没有版本管理来确保反序列化器匹配序列化数据的格式。您可以添加版本信息,但请注意,这是一组非常低级的函数——它期望反序列化器知道正确的值类型以及它们是否可空,并且顺序正确。如果顺序错误或类型不匹配,反序列化器将生成错误的数据,并且最有可能在尝试解码输入流时失败。同样,可以添加类型信息——事实上,标准的“值类型”可以与 null 标志字节一起编码,但我选择不这样做,特别是为了减小生成的数据集大小。如果您认为需要这种额外的保护层,请随时添加。

Null 支持

序列化器支持 nullDBNull.Value 值,但这些是可选的。如果您需要支持 null 值,则会在该值旁边添加一个额外的字节,用于指示该值是 null 还是 DBNull.Value。显然,反序列化器也需要指定它是否期望 null 值——序列化和反序列化在值类型和可空性方面必须始终同步(这是个新词!)。

我曾考虑过一个优化,但没有实现,那就是为 DataTable 中的每一行提供一个位字段头,其中的位字段指示其关联的字段是否为空。例如,这将为每八个字段节省四个字节(需要 2 位来管理“非空”、“null”或“DBNull.Value”)。这并不是一个很大的节省,特别是如果您选择使用剩余的每字段 6 位来描述编码的字段类型,如上所述。因此,我选择了简单的解决方案,而不是将自己(和您)置于一个困境。

理解装箱值类型与可空值类型之间的区别

装箱值类型(object foo)和可空值类型(int? foo)之间的显著区别在于,装箱值类型支持 nullDBNull.Value “值”,而可空值类型仅支持 null。装箱值类型序列化在序列化来自数据库的数据时很有用,因为这些数据可能包含 DBNull.Value 值。

示例用法

在深入代码之前,我将通过一些单元测试来演示如何使用这些类(代码包含全面的单元测试,这里我将选择一些特定的示例)。这些单元测试使用我的 AUT 引擎编写。这将让您对序列化器能做什么有一个初步的认识。每个测试都有一个设置例程,用于初始化序列化器、反序列化器和内存流。

[TestFixture]
public class ValueTypeTests
{
  MemoryStream ms;
  RawSerializer rs;
  RawDeserializer rd;

  [SetUp]
  public void Setup()
  {
    ms = new MemoryStream();
    rs = new RawSerializer(ms);
    rd = new RawDeserializer(ms); 
  }
  ...
}

简单值类型序列化

[Test]
public void Int()
{
  int val=int.MaxValue;
  rs.Serialize(val);
  rs.Flush();
  ms.Position=0;
  val=rd.DeserializeInt();
  Assertion.Assert(val==int.MaxValue, "int failed");
}

第一个测试演示了整数的直接序列化。不出所料,内存流的长度为 4 字节。

装箱序列化

[Test]
public void Int()
{
  int val = int.MaxValue;
  rs.Serialize((object)val);
  rs.Flush();
  ms.Position = 0;
  val = rd.DeserializeInt();
  Assertion.Assert(val == int.MaxValue, "int failed");
}

在此测试中,我们序列化一个装箱值,并在知道所需类型的情况下将其反序列化。此测试会执行序列化器中不同的路径。它也是通向下一个测试的过渡。同样,内存流的长度为 4 字节。

装箱可空值类型

[Test]
public void BoxedNullable()
{
  object anInt = 5;
  object aNullInt = null;
  rs.SerializeNullable(anInt);
  rs.SerializeNullable(aNullInt);
  rs.Flush();
  ms.Position = 0;
  anInt = rd.DeserializeNullable(typeof(int));
  aNullInt = rd.DeserializeNullable(typeof(int));
  Assertion.Assert((int)anInt == 5, "non-null nullable failed.");
  Assertion.Assert(aNullInt == null, "null nullable failed.");
}

在此测试中,序列化了两个装箱的 int,第一个带值,第二个赋值为 null。使用 SerializeNullable 方法告诉序列化器该值类型可能是 null。序列化后,内存流长度为 6 字节。为什么?第一个值使用标志字节进行序列化,因此占用 5 字节。第二个值是 null,因此只占用标志字节。

可空值类型

[Test]
public void Int()
{
  int? val1=int.MaxValue;
  int? val2=null;
  rs.Serialize(val1);
  rs.Serialize(val2);
  rs.Flush();
  ms.Position=0;
  val1=rd.DeserializeNInt();
  val2=rd.DeserializeNInt();
  Assertion.Assert(val1==int.MaxValue, "non-null nullable int failed");
  Assertion.Assert(val2==null, "null nullable int failed");
}

在这里,我们使用了 C# 2.0 中支持的新可空值类型。生成的内存流长度也为 6 字节。请注意,使用了不同的反序列化方法来返回适当的可空值类型。您也可以将此反序列化为类型为 int 的对象。

[Test]
public void IntObject()
{
  int? val1 = int.MaxValue;
  int? val2 = null;
  rs.Serialize(val1);
  rs.Serialize(val2);
  rs.Flush();
  ms.Position = 0;
  object obj1 = rd.DeserializeNullable(typeof(int));
  object obj2 = rd.DeserializeNullable(typeof(int));
  Assertion.Assert((int)val1 == int.MaxValue, "non-null nullable int failed");
  Assertion.Assert(val2 == null, "null nullable int failed");
}

数据表

以下单元测试演示了如何序列化 DataTable。我已特意未将此代码包含在原生序列化类中,因为序列化 DataTable 的方法可能是应用程序特定的。

测试数据

测试夹具的 DataTable 使用以下数据进行初始化:

[TestFixtureSetUp]
public void FixtureSetup()
{
  dt = new DataTable();
  dt.Columns.Add(new DataColumn("pk", typeof(Guid)));
  dt.Columns.Add(new DataColumn("LastName", typeof(string)));
  dt.Columns.Add(new DataColumn("FirstName", typeof(string)));
  dt.Columns.Add(new DataColumn("MiddleInitial", typeof(char)));
  dt.Columns["pk"].AllowDBNull = false;
  dt.Columns["LastName"].AllowDBNull = false;
  dt.Columns["FirstName"].AllowDBNull = false;
  dt.Columns["MiddleInitial"].AllowDBNull = true;

  DataRow dr=dt.NewRow();
  dr["pk"]=Guid.NewGuid();
  dr["LastName"]="Clifton";
  dr["FirstName"]="Marc";
  dr["MiddleInitial"] = DBNull.Value;
  dt.Rows.Add(dr);

  dr=dt.NewRow();
  dr["pk"]=Guid.NewGuid();
  dr["LastName"]="Clifton";
  dr["FirstName"]="Ian";
  dr["MiddleInitial"] = DBNull.Value;
  dt.Rows.Add(dr);

  dr=dt.NewRow();
  dr["pk"]=Guid.NewGuid();
  dr["LastName"]="Linder";
  dr["FirstName"]="Karen";
  dr["MiddleInitial"] = 'J';
  dt.Rows.Add(dr);

  dt.AcceptChanges();
}

序列化和反序列化 DataTable

以下是验证数据表序列化的单元测试。请注意,AllowDBNull 属性是如何用于确定被序列化的对象是否允许 null。您还将看到我序列化了表名、列数和行数,以及列名和类型。这些信息构成了实际表数据的头部。另请注意,使用了程序集限定名称。在此示例中,这意味着接收端需要与序列化数据时相同的 .NET 版本。仅使用名称,可以有一个版本的 .NET 序列化数据,而另一个版本反序列化它。这取决于您和您想要实现的目标,这也是为什么此代码不包含在下载的原生序列化类中的原因。

[Test]
public void DataTable()
{
  rs.Serialize(dt.TableName);
  rs.Serialize(dt.Columns.Count);
  rs.Serialize(dt.Rows.Count);

  foreach (DataColumn dc in dt.Columns)
  {
    rs.Serialize(dc.ColumnName);
    rs.Serialize(dc.AllowDBNull);
    rs.Serialize(dc.DataType.AssemblyQualifiedName);
  }

  foreach (DataRow dr in dt.Rows)
  {
    foreach (DataColumn dc in dt.Columns)
    {
      if (dc.AllowDBNull)
      {
        rs.SerializeNullable(dr[dc]);
      }
      else
      {
        rs.Serialize(dr[dc]);
      }
    }
  }

  rs.Flush();
  ms.Position = 0;

  // Deserialize

  string tableName = rd.DeserializeString();
  int columns = rd.DeserializeInt(); 
  int rows = rd.DeserializeInt();

  Assertion.Assert(columns == 4, "Column count is wrong.");
  Assertion.Assert(rows == 3, "Row count is wrong.");

  DataTable dtIn = new DataTable();

  for (int x = 0; x < columns; x++)
  {
    string columnName = rd.DeserializeString();
    bool allowNulls = rd.DeserializeBool();
    string type = rd.DeserializeString();

    DataColumn dc = new DataColumn(columnName, Type.GetType(type));
    dc.AllowDBNull = allowNulls;
    dtIn.Columns.Add(dc);
  }

  for (int y = 0; y < rows; y++)
  {
    DataRow dr = dtIn.NewRow();

    for (int x = 0; x < columns; x++)
    {
      DataColumn dc=dtIn.Columns[x];
      object obj;

      if (dc.AllowDBNull)
      {
        obj = rd.DeserializeNullable(dc.DataType);
      }
      else
      {
        obj = rd.Deserialize(dc.DataType);
      }

      dr[dc] = obj;
    }

    dtIn.Rows.Add(dr);
  }

  for (int y = 0; y < rows; y++)
  {
    for (int x = 0; x < columns; x++)
    {
      Assertion.Assert(dt.Rows[y][x].Equals(dtIn.Rows[y][x]),
               "Deserialized data does not match serialized data");
    }
  }
}

加密流

如果您想为序列化流添加加密,这里有一个关于它是如何工作的示例:

[Test]
public void EncryptionStreaming()
{
  // string -> RawStreamEncoder -> Encryptor -> MemoryStream
  MemoryStream ms = new MemoryStream(); // final destination stream
  EncryptTransformer et = new EncryptTransformer(EncryptionAlgorithm.Des);
  ICryptoTransform ict = et.GetCryptoServiceProvider(null);
  CryptoStream encStream = new CryptoStream(ms, ict, CryptoStreamMode.Write);
  RawSerializer rs=new RawSerializer(encStream);   
    rs.Serialize("Hello World"); // serialize
  ((CryptoStream)encStream).FlushFinalBlock(); // MUST BE APPLIED!
  ms.Position=0;

  // MemoryStream -> Decryptor -> RawStreamDecoder -> string
  DecryptTransformer dt = new DecryptTransformer(EncryptionAlgorithm.Des);
  dt.IV = et.IV;
  ict = dt.GetCryptoServiceProvider(et.Key);
  CryptoStream decStream = new CryptoStream(ms, ict, CryptoStreamMode.Read);
  RawDeserializer rd=new RawDeserializer(decStream); 
  string str=(string)rd.Deserialize(typeof(string)); // Gets the data.
  Assertion.Assert(str=="Hello World", "Unexpected return.");
}

压缩流

或者,假设您希望流被压缩。此示例利用了 .NET 2.0 框架中的压缩流。

[Test]
public void CompressionStreaming()
{
  // string -> RawStreamEncoder -> Compressor -> MemoryStream
  MemoryStream ms=new MemoryStream(); // final destination stream
  GZipStream comp = new GZipStream(ms, CompressionMode.Compress, true);
  RawSerializer rs=new RawSerializer(comp);
  rs.Serialize("Hello World"); // serialize
  comp.Close(); // outputs last part of the data

  ms.Position=0;

  // MemoryStream -> Decompressor -> RawStreamDecoder -> string
  GZipStream decomp = new GZipStream(ms, CompressionMode.Decompress);

  RawDeserializer rd=new RawDeserializer(decomp); 
    string str=(string)rd.Deserialize(typeof(string)); // Gets the data.
  Assertion.Assert(str=="Hello World", "Unexpected return.");
}

压缩-加密流

当然,您可能希望压缩并加密您的数据流。

[Test]
public void CompressionEncryptionStreaming()
{
  // string -> RawStreamEncoder -> Compressor -> Encryptor -> MemoryStream
  MemoryStream ms=new MemoryStream(); // final destination stream

  EncryptTransformer et = new EncryptTransformer(EncryptionAlgorithm.Des);
  ICryptoTransform ict = et.GetCryptoServiceProvider(null);
  CryptoStream encStream = new CryptoStream(ms, ict, CryptoStreamMode.Write);

  GZipStream comp = new GZipStream(encStream, CompressionMode.Compress, true);
  RawSerializer rs = new RawSerializer(comp);
     rs.Serialize("Hello World"); // serialize
  comp.Close(); // must close to get final bytes
  ((CryptoStream)encStream).FlushFinalBlock(); // MUST BE APPLIED!

  // Reset the position and read the stream back in.
  ms.Position=0;

  // MemoryStream -> Decryptor -> Decompressor -> RawStreamDecoder -> string
  DecryptTransformer dt = new DecryptTransformer(EncryptionAlgorithm.Des);
  dt.IV = et.IV;
  ict = dt.GetCryptoServiceProvider(et.Key);
  CryptoStream decStream = new CryptoStream(ms, ict, CryptoStreamMode.Read);

  GZipStream decomp = new GZipStream(decStream, CompressionMode.Decompress);

  RawDeserializer rd = new RawDeserializer(decomp); 
  string str=(string)rd.Deserialize(typeof(string)); // Gets the data.
  Assertion.Assert(str=="Hello World", "Unexpected return.");
}

附录

为了避免在文章开头充斥着底层细节和其他问题,我决定将其中一些放在附录中。

错误的工具

对于我客户的需求,BinaryFormatter 根本就是错误的工具。这是 MSDN 对它的评价:

SoapFormatterBinaryFormatter 类实现了 IRemotingFormatter 接口以支持远程过程调用(RPC),并实现了 IFormatter 接口(由 IRemotingFormatter 继承)以支持对象图的序列化。SoapFormatter 类也支持与 ISoapMessage 对象的 RPC,而不使用 IRemotingFormatter 功能。

首先,我们不需要支持远程过程调用。其次,我们不需要对象图序列化的完整支持。例如,在我们的应用程序中,表示缓存数据的 DataTable 可以传输到客户端而无需任何头部信息,因为有一个单独的数据字典定义了表列和标志。(在本文提供的序列化 DataTable 的代码中,我确实有一个头部块。)

在查看序列化 DataTable 的复杂性之前,让我们来看一个非常简单的例子——序列化一个 bool

using System;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
using System.Text;

namespace BinaryFormatterTests
{
  class Program
  {
    static void Main(string[] args)
    {
      MemoryStream ms = new MemoryStream();
      BinaryFormatter bf = new BinaryFormatter();
      bool flag = false;
      bf.Serialize(ms, flag);
      byte[] data = ms.ToArray();
      Console.WriteLine("Done.");
    }
  }
}

这会生成 53 字节。

这里的 bool 在哪里?它位于倒数第二个字节。如果设置为 true,最后三个字节读取:01 01 0b。

如果我们添加第二个 bool 呢?其中有多少是初始头部,有多少是实际数据?嗯,事实证明,没有任何是初始头部。如果我们序列化第二个 bool(只需两次调用 bf.Serailize(ms, flag);),生成的内存流现在大小翻倍:106 字节!

情况变得更糟。我们现在来看看 DataTable。一个空的 DataTable 需要初始 1051 字节才能序列化。添加三个列定义(一个 GUID 和两个字符串)需要额外 572 字节。每增加一行(字符串中没有数据)需要额外 85 字节(对于一个空的 GUID 和两个空的字符串!)。如果你真的在这些行中有数据,那后果不堪设想!

static void Main(string[] args)
{
  StringBuilder sb=new StringBuilder();
  StringWriter sw=new StringWriter(sb);
  XmlSerializer xs = new XmlSerializer(typeof(DataTable));

  DataTable dt = new DataTable("Foobar");
  DataColumn dc1 = new DataColumn("ID", typeof(Guid));
  DataColumn dc2 = new DataColumn("FirstName", typeof(string));
  DataColumn dc3 = new DataColumn("LastName", typeof(string));
  dt.Columns.Add(dc1);
  dt.Columns.Add(dc2); 
  dt.Columns.Add(dc3);

  DataRow dr = dt.NewRow();
  dr["ID"] = Guid.NewGuid();
  dr["FirstName"] = "Marc";
  dr["LastName"] = "Clifton";
  dt.Rows.Add(dr);

  dr = dt.NewRow();
  dr["ID"] = Guid.NewGuid();
  dr["FirstName"] = "Karen";
  dr["LastName"] = "Linder";  
  dt.Rows.Add(dr);

  xs.Serialize(sw, dt);
  string str = sb.ToString();
  Console.WindowWidth = 100;
  Console.WriteLine(str.Length);
  Console.WriteLine(str);
}

向两个行添加 GUID 和“Marc”、“Clifton”以及“Karen”、“Linder”,序列化输出增加到惊人的 1982 字节,增加了 274 字节来表示两行数据,而如果理想存储,这些数据不应超过 40 字节左右(如果使用 Unicode 表示字符串,则为 60 字节)。

为什么不使用 XML 序列化?

我那个两行示例生成的 XML 数据实际上更小,为 1671 字节,并且由于其高令牌化率,压缩效果很好。

但是,让我们看看 XML 处理 null 值时的一个问题。假设这些行中的一些包含 null 值。我们将同时查看 DBNull.Value 和简单地将字段设置为 null

  DataRow dr = dt.NewRow();
  dr["ID"] = Guid.NewGuid();
  dr["FirstName"] = "Marc";
  dr["LastName"] = DBNull.Value;
  dt.Rows.Add(dr);

  dr = dt.NewRow();
  dr["ID"] = Guid.NewGuid();
  dr["FirstName"] = "Karen";
  dr["LastName"] = null;  
  dt.Rows.Add(dr);

生成的输出是:

嗯。LastName 完全缺失。现在,当我们将此反序列化到新的 DataTable 中时会发生什么?结果是两行的 LastName 字段都被设置为 DBNull 类型。虽然这在序列化数据库表的上下文中是合适的,但在其他上下文中可能不是您想要或期望的!如果您测试 null == DBNull.Value,结果是 false!(事实上,在 nullDBNull 和空字符串的复杂性,在控件属性值(如 TextBox.Text 属性)的上下文中,并将这些属性直接绑定到 DataTable 字段,这是另一篇文章本身。)

这个问题比您想象的更棘手。例如,考虑这个类:

public class Test
{
  protected string str=String.Empty;

  public string Str
  {
    get { return str; }
    set { str = value; }
  }
}

在这里,程序员已经初始化了“str”。但是,如果“str”在某个时候被设置为 null 然后被序列化,则该属性不会被发出。在反序列化时,“str”未被赋值,因此保留其初始值——在本例中为空字符串。同样,这也不是您可能期望的,而且绝对不是显而易见的事情,您会认为它是一个可能的问题。

因此,XML 序列化不是对称的,因为它不能正确处理 null 引用。根据您的需求,您可能会发现这是一个问题。更重要的是,XML 序列化仍然会产生一个大文件,需要进行压缩后处理。

压缩与扩展

既然说到这个话题,让我给您一些基准测试:使用 #ziplib:一个 27MB 的文件在我的测试机器上需要 33 秒才能压缩到 5.1MB。在 XP 上点击“发送到压缩(zip)文件夹”,需要 3 秒钟创建一个 5.6MB 的压缩文件。无需多言,#ziplib 优化得不好。对于我们通常处理的小数据集来说,压缩也不是那么有价值,特别是当我们考虑在网络上传输的数据包的序列化时。然而,BinaryFormatter 产生的扩展数据是完全不可接受的。如果我们使用压缩,是因为我们想压缩数据本身。如果我们看看 XML 序列化,我们立刻会想“哦,那会很好地压缩”,但这基本上是错误的想法,因为我们识别为可压缩的信息主要是 *元数据*!原生序列化的目的就是去除元数据!

AcceptChanges 的重要性

您会在上面的第一个 XML 屏幕截图中注意到“hasChanges”属性。这提出了一个重要的一点——在序列化 DataTable 之前,请确保调用 AcceptChanges,否则您会得到这些 diffgram 条目,这可能不是您想要的。通过在上面的 BinaryFormatter 测试中调用 AcceptChanges,我节省了 60 字节。

致谢

我想感谢 Justin Dunlap 在 BinaryReader/Writer 方面给了我正确的指导,以及他在结构封组代码方面的工作。

最后说明

在研究过程中,我发现了一个来自 XCEED 的第三方库,它支持压缩、加密、二进制序列化等。我没有评估过他们的产品,但任何对专业解决方案感兴趣的人都应该看看他们的产品,并且我确定还有其他软件包。开发者许可、源代码访问以及库中您不需要但最终成为额外负担的附加功能,与预先测试过的、受支持产品的便利性之间总有一个平衡。对于“简单”的事情,比如原生序列化,我倾向于自己动手,因为我能学到很多东西,而且坦率地说,我仍然需要花同样的时间编写测试例程来验证第三方包是否符合我的需求。结果是,我得到了一段简短简单的代码,我认为它很容易维护。

© . All rights reserved.