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

优化 .NET 中的序列化 - 第 2 部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (33投票s)

2006 年 10 月 8 日

公共领域

33分钟阅读

viewsIcon

338815

downloadIcon

3777

提供代码和技术,使开发人员能够优化 DataSet/DataTable 的序列化。

引言

这是关于优化序列化(尤其是在远程处理中使用)的两个系列文章中的第二篇。

在本文中,我们将通过一个完整且真实的示例,演示如何使用第一部分(此处)中介绍的快速序列化代码来序列化 DataSet 和 DataTable,包括类型化变体。

  1. 首先,我们将查看一些测试结果,以了解采用此代码是否真正值得。
  2. 然后,我将描述代码的基本用法
  3. 接下来是如何将其集成到 .Net 远程处理基础架构中
  4. 最后,我将详细介绍代码的工作原理以及如何在非 DataSet 类上使用相同或类似的技术。

1. 测试结果

我使用各种大小的源数据生成了一些测试结果,以了解您可能期望达到的尺寸和时间缩减幅度。

  • 花费的时间包括序列化和反序列化,精度达到毫秒。
  • 时间平均为 10 次运行,加上一次初始的非计时 JIT 传递。
  • 在每次传递之间执行完整的垃圾回收(非计时),以确保仅计时序列化/反序列化例程。

Northwind - 仅表

一个包含 Northwind 所有 13 个表的 DataSet。

方法 尺寸(字节) 花费时间(秒)
标准 .NET 序列化 1,431,297 0.560
快速序列化 383,001 0.031
小 73.2% 快 18.1 倍

Northwind - 表和视图

一个包含 Northwind 所有 13 个表和 16 个视图的 DataSet。

方法 尺寸(字节) 花费时间(秒)
标准 .NET 序列化 5,635,208 2.624
快速序列化 688,910 0.126
小 87.8% 快 20.8 倍

大型引用数据表测试 - 35,125 行,15 列

一个 DataSet,其中包含一个加载了大型引用数据库表的 DataTable,该表包括:

  • 4 个 int 列
  • 1 个 varchar(15) 列
  • 1 个 varchar(50) 列
  • 2 个 varchar(30) 列
  • 1 个 varchar(6) 列
  • 1 个 varchar(500) 列
  • 4 个 char(1) 列
  • 1 个 datetime 列

方法 尺寸(字节) 花费时间(秒)
标准 .NET 序列化 16,736,432 7.402
快速序列化 2,340,840 0.571
  小 86.0% 快 12.96 倍

海量引用数据视图测试 - 39,071 行,91 列

一个 DataSet,其中包含一个加载了海量数据库视图的 DataSet,该视图包括:

  • 13 个 bit 列
  • 6 个 char(1) 列
  • 2 个 char(2) 列
  • 1 个 char(3) 列
  • 12 个 datetime 列
  • 19 个 int 列
  • 1 个 numeric(10,2) 列
  • 5 个 numeric(10,4) 列
  • 4 个 numeric(15,4) 列
  • 6 个 numeric(4,2) 列
  • 1 个 varchar(10) 列
  • 3 个 varchar(20) 列
  • 1 个 varchar(255) 列
  • 1 个 varchar(3) 列
  • 5 个 varchar(5) 列
  • 7 个 varchar(50) 列
  • 1 个 varchar(500) 列
  • 3 个 varchar(70) 列

如果将包含此数据的 DataSet 的 XML 输出写入文件,它将占用巨大的 112MB 和 2,118,900 行!

对于快速序列化器来说没有问题,但 .NET 无法序列化如此大的 DataSet,并因 OutOfMemoryException 而崩溃。

方法 尺寸(字节) 花费时间(秒)
标准 .NET 序列化 <失败> <失败>
快速序列化 6,513,586 2.960

从这些结果来看,很明显快速优化总是更快,并且总是产生更小的尺寸。对于少量数据,标准的 .Net 序列化可能足够,但它不像快速优化那样具有良好的可伸缩性,并且随着序列化数据的增多,时间和尺寸的差异变得更加明显,直到标准的 .Net 序列化器根本无法处理并抛出异常。

需要注意的是,流压缩(通过自定义 Sink)可以将最终数据尺寸减小到此处显示的最小尺寸的一半以下。但是,您可能会发现,通过快速序列化生成的更小尺寸已经足够快,并且压缩的开销可能不值得,除非可能是在已知慢速连接上进行传输。

2. 使用代码

代码下载包含一个名为 AdoNetHelper 的类。它具有许多静态方法,可为受支持的 ADO.Net 对象提供序列化和反序列化服务。还有几个非序列化特定辅助方法,这就是我创建单个辅助类而不是单独类的原因 - 我喜欢将通用辅助代码保存在一个地方。

简而言之,对于序列化,您传入要序列化的 ADO.Net 对象,并获得一个字节数组。对于反序列化,您传入字节数组(以及一个空的 ADO.Net 对象或一个将要实例化的 Type),然后获得完全填充的 ADO.Net 对象。

支持的 ADO.Net 对象包括 DataSetDataTable、类型化的 DataSet 和类型化的 DataTable。还支持“简单”DataTable,其中“简单”定义为已知的包含所有未修改行且没有错误的类型化 DataTable。这使我们能够仅序列化原始数据本身,而无需任何基础架构开销,例如 RowStateColumns 等。

DataSet

  public static byte[] SerializeDataSet(DataSet dataSet)
  public static DataSet DeserializeDataSet(byte[] serializedData)

一个普通的 DataSet 很容易序列化 - 一个序列化方法和一个反序列化方法。

所有基础架构都将被存储,包括表、列、行、约束、扩展属性、XML 命名空间等。

类型化的 DataSet

  public static byte[] SerializeTypedDataSet(DataSet dataSet)
  public static DataSet DeserializeTypedDataSet(Type dataSetType,
                                                byte[] serializedData)
  public static DataSet DeserializeTypedDataSet(DataSet dataSet,
                                                byte[] serializedData)

类型化的 DataSet 是使用 Microsoft MSDataSetGenerator 从 .XSD 架构生成的。它已经包含基础架构,因此序列化只需要存储预定义表中实际数据的存储。

我发现,与其直接使用生成的 DataSet 类,不如从它派生一个新类,并在此之前编写所有代码。这样,我可以添加方法、接口等,而无需担心它们在架构更改时被覆盖,并为远程处理目的轻松集成我们的序列化代码。

例如,如果我有一组名为 XXX 的数据,我将架构文件命名为 XXXDataSetSchema.xsd,它会自动生成 XXXDataSetSchema.cs;然后我创建 XXXDataSet.cs,其中包含一个名为 XXXDataSet 的类,并继承自 XXXDataSetSchema。应用程序中仅使用 XXXDataSet

对于反序列化,您可以传入类型化的 DataSet 的 Type(在这种情况下会创建一个),或者一个预先实例化的对象 - 然后数据将恢复到预先创建的表中。

DataTable

  public static byte[] SerializeDataTable(DataTable dataTable)
  public static DataTable DeserializeDataTable(byte[] serializedData)

用法与普通 DataSet 非常相似。DataTable 可以独立存在,也可以是现有 DataSet 的一部分,但如果它是 DataSet 的一部分,则仅序列化唯一约束;任何外键约束都会被忽略。

类型化的 DataTable

  public static byte[] SerializeTypedDataTable(DataTable dataTable)
  public static DataTable DeserializeTypedDataTable(DataTable dataTable,
                                                byte[] serializedData)
  public static DataTable DeserializeTypedDataTable(Type dataTableType,
                                                byte[] serializedData)

以与类型化 DataSet 相同的方式子类化生成的类型化 DataTable 实际上并不可行。因此,虽然这些辅助方法可以轻松生成序列化的字节数组,但要让远程处理使用这些方法会更棘手一些,但并非不可能 - 请参阅下一节。

简单 DataTable

  public static byte[] SerializeSimpleTable(DataTable dataTable)
  public static DataTable DeserializeSimpleDataTable(DataTable dataTable,
                                                byte[] serializedData)
  public static DataTable DeserializeSimpleDataTable(Type dataTableType,
                                                byte[] serializedData)

用法与类型化 DataTable 完全相同,但其目的是已知这些表没有错误(在行和列级别),并且所有 RowStates 都应该是 Unchanged,就像它们刚刚由数据库查询填充一样。

事实上,该例程在 Added 和 Modified 行(但不是 Deleted 行 - 那会抛出异常)时也会运行,但在反序列化时,所有行的 RowState 都将是 Unchanged。

此方法集也可用于不是从 XSD 文件生成的其他 DataTable。例如,LLBLGenPro 具有 TypedViewTypedList 的概念,它们都派生自 DataTable 并用于只读数据,但使用其内部架构来生成 Columns 等基础架构。只要 DataTable 基础架构已经配置好,这些反序列化方法就会重新填充所有数据。

所有这些辅助方法,在进行适当的参数验证后,都使用内部嵌套类来执行实际工作并返回结果。就目前而言,您可以直接在远程处理中使用它们,方法是传递生成的字节数组而不是服务接口中的实际对象。但是,这会使代码难以阅读或理解,所以让我们看看使用此代码的替代方法。

3. 在远程处理中的使用

现在我们有了一种将 ADO.Net 可序列化对象放入字节数组并反之的方法。现在我们需要一种方法让 .Net Remoting 使用我们的序列化代码,而不是让 DataSet 进行 XML 序列化。

不幸的是,没有办法使其“完全”透明,因为我们无法修改 DataSet/DataTable 源代码。但我们可以根据您想/能够更改源代码的程度,选择不同的方法来实现。

我们必须以某种方式实现 ISerializable 接口,以便由 Remoting 运行时创建的 BinaryFormatter 能够控制我们自己进行序列化。有两种方法可以做到这一点:一种是直接在要序列化的对象上实现 ISerializable,另一种是使用代理(surrogate),代理是一个外部类,因此不需要修改对象本身。

在两种情况下,都需要实现两个方法。一个 GetObjectData 方法用于序列化,一个构造函数用于反序列化。实际上,对于代理对象,这将是一个 SetObjectData,但原理是相同的 - 您会获得正确类型的对象,但它是完全未初始化的,您有责任完成配置对象所需的任何工作。

让我们先看看普通 DataSets 的方法

FastSerializableDataSet

最简单的方法是创建一个从 DataSet 派生的新类,该类除了实现 ISerializable 以在序列化时获得控制之外,不做任何其他事情。

这是一个示例类(包含在下载中)

[Serializable]
public class FastSerializableDataSet: DataSet, ISerializable
{
    #region Constructors
    public FastSerializableDataSet(): base() {}
    public FastSerializableDataSet(string dataSetName): base(dataSetName) {}
    #endregion Constructors

    #region ISerializable Members
    protected FastSerializableDataSet(SerializationInfo info,
                                      StreamingContext context)
    {
        AdoNetHelper.DeserializeDataSet(this,
                               (byte[]) info.GetValue("_", typeof(byte[])));
    }

    public void GetObjectData(SerializationInfo info,
                              StreamingContext context)
    {
        info.AddValue("_", AdoNetHelper.SerializeDataSet(this));
    }
    #endregion
}

真的很简单,因为它归结为两行 - 一行用于序列化,一行用于反序列化。

如果您的项目可以更改,将所有对 DataSet 的引用替换为 FastSerializableDataSet,那么您就可以开始了!

然而,生活很少如此简单,并且可能不是所有人都接受更改对 DataSet 的引用,那么是否有更少侵入性的方法?

WrappedDataSet

如果您无法更改代码中的所有 DataSet 引用,您可能可以只更改远程接口中出现的那些。

下面是一个“包装”普通 DataSet 的类,它提供了重载的隐式运算符,以便您可以在拥有 WrappedDataSet 引用的地方分配/传递 DataSet。WrappedDataSet 实现 ISerializable,其实现只是序列化包装的 DataSet。通过在接口声明中使用 WrappedDataSet 而不是 DataSet,您可以在不更改应用程序中其他引用的情况下使用快速序列化。

这是类

[Serializable]
public class WrappedDataSet: ISerializable
{
#region Casting Operators
    public static implicit operator DataSet (WrappedDataSet wrappedDataSet)
    {
        return wrappedDataSet.DataSet;
    }

    public static implicit operator WrappedDataSet (DataSet dataSet)
    {
        return new WrappedDataSet(dataSet);
    }
#endregion Casting Operators


#region Constructors
    public WrappedDataSet(DataSet dataSet)
    {
        if (dataSet == null) throw new ArgumentNullException("dataSet");
        this.dataSet = dataSet;
    }
#endregion Constructors


#region Properties
    public DataSet DataSet {
        get { return dataSet; }
    } DataSet dataSet;
#endregion Properties


#region ISerializable Members
    protected WrappedDataSet(SerializationInfo info, StreamingContext context)
    {
        dataSet = AdoNetHelper.DeserializeDataSet((byte[]) info.GetValue("_",
                                                  typeof(byte[])));
    }

    public void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        info.AddValue("_", AdoNetHelper.SerializeDataSet(dataSet));
    }
#endregion
}

TypedDataSets

类型化的 DataSet 已经拥有反序列化所需的构造函数,但它与普通的 DataSet 反序列化绑定在一起,而我们想要绕过它,所以它对我们没有用。

还有一个私有的 InitClass 方法,它创建表/关系(进而创建列/约束)基础架构等。现在我们需要使用此方法,因为我们编写的任何替换反序列化构造函数都将反序列化一个完全*未初始化*的对象。

如果您遵循我上面关于从生成的 DataSet 代码派生而不是直接使用它的建议,那么我们只需要在派生类上实现 ISerialization

#region ISerializable Members
protected DerivedFromGeneratedDataSet(SerializationInfo info,
                                      StreamingContext context)
{
   AdoNetHelper.DeserializeTypedDataSet(this,
                                  (byte[]) info.GetValue("_", typeof(byte[])));
}

void ISerializable.GetObjectData(SerializationInfo info,
                                 StreamingContext context)
{
   info.AddValue("_", AdoNetHelper.SerializeTypedDataSet(this));
}
#endregion

在反序列化时,这将调用基类无参构造函数,该构造函数将运行 InitClass 为我们设置基础架构。我们只需要将自身和序列化数据传递给辅助方法即可完成工作。

代理 (Surrogates)

如果以上两种方法都不合适,那么唯一剩下的选择就是使用代理对象在更低级别进行控制以执行序列化/反序列化。

原则上,这应该相对简单:代理对象是一个实现 ISerializationSurrogate 的类,它“知道”如何为给定类型执行序列化,并且它被“集成”到远程处理/序列化过程中,以代替正常的基于反射的方式来实际执行工作。这通过实现 ISurrogateSelector 的类来实现。

如果您想使用 BinaryFormatter 直接执行此操作,那么代码非常简单,因为其中一个构造函数接受一个 ISurrogateSelector 对象。该对象决定哪个 Surrogate(如果有)可以处理给定的 Type,并在请求时向 Binary Formatter 提供该 Surrogate 的实例。

然而,问题在于 Microsoft 只公开了 Remoting 的部分功能,而没有公开其他部分。通过 Reflector 进行一些挖掘显示,它会创建一个新的 BinaryFormatter,然后将一个新的 RemotingSurrogateSelector 附加到它,但遗憾的是,这些对象都无法从外部访问或配置。

并非一切都已丢失,我们可以通过使用自定义 Sink 来解决这个问题。CodeProject 上有很多优秀的文章(例如,.NET Remoting Customization Made Easy: Custom Sinks),它们描述了如何在格式化器 Sink 之前和之后插入自定义 Sink,但我们需要做的是替换格式化器 Sink 本身。

在下载中,我包含了一些示例类来执行此操作

  • CustomBinaryClientFormatterSinkProvider
  • CustomBinaryClientFormatterSink
  • CustomBinaryServerFormatterSinkProvider
  • CustomBinaryServerFormatterSink

它们是通过使用 Reflector 检查 Microsoft 代码编写的,并删除了所有非 Http 代码(我只使用 Http 通道)和非必需部分,例如 TypeFilterLevel(始终为 Full)等。对不可访问的内部代码的调用是通过在类中直接复制不可访问的代码或替换等效类(例如,MemoryStream 而不是不可访问的 ChunkedMemoryStream)来实现的。每当创建 BinaryFormatter 时,我都会确保我们的 Surrogate Selector 成为链的一部分。

要在应用程序中使用,您可以选择使用 App.config 或编写手动代码
(请记住,客户端上的端口可能为“0”)

App.Config 文件配置

 <system.runtime.remoting>
  <application>
   <channels>
    <channel ref="http" port="999">
     <clientProviders>
      <formatter
     type="SimmoTech.Utils.Remoting.CustomBinaryClientFormatterSinkProvider,
SimmoTech.Utils"/>
     </clientProviders>
     <serverProviders>
      <formatter
      type="SimmoTech.Utils.Remoting.CustomBinaryServerFormatterSinkProvider,
SimmoTech.Utils"/>
     </serverProviders>
    </channel>
   </channels>
  </application>
 </system.runtime.remoting>

然后在应用程序的开始处或附近使用此行

RemotingConfiguration.Configure(
                AppDomain.CurrentDomain.SetupInformation.ConfigurationFile);

代码配置

   CustomBinaryServerFormatterSinkProvider serverProvider
                          = new CustomBinaryServerFormatterSinkProvider();
   CustomBinaryClientFormatterSinkProvider clientProvider
                          = new CustomBinaryClientFormatterSinkProvider();

   IDictionary properties = new Hashtable();
   properties["port"] = 999;

   HttpChannel channel = new HttpChannel(properties, clientProvider,
                                         serverProvider);
   ChannelServices.RegisterChannel(channel);

ISerializationSurrogate 实现的代码不需要状态,因此我们可以将 ISurrogateSelector 和 ISerializationSurrogate 的功能合并到一个名为 AdoNetFastSerializerSurrogate 的类中。

ISerializationSurrogate 实现的主要代码如下所示:

public ISerializationSurrogate GetSurrogate(Type type,
                  StreamingContext context, out ISurrogateSelector selector)
{
   if (typeof(DataSet).IsAssignableFrom(type) ||
      typeof(DataTable).IsAssignableFrom(type))
   {
      selector = this;
      return this;
   } else
   {
      selector = null;
      return null;
   }
}

这只是表示如果类型是 DataSetDataTable(或派生自其中之一),则将其指示为 ISurrogateSelector,并将其作为 ISerializationSurrogate 返回。

序列化代码如下所示:

public void GetObjectData(object obj, SerializationInfo info,
                          StreamingContext context)
{
   byte[] data;
   if (obj.GetType() == typeof(DataSet) || obj is IModifiedTypedDataSet )
    data = AdoNetHelper.SerializeDataSet(obj as DataSet);
   else if (obj.GetType() == typeof(DataTable))
    data = AdoNetHelper.SerializeDataTable(obj as DataTable);
   else if (obj is DataSet)
    data = AdoNetHelper.SerializeTypedDataSet(obj as DataSet);
   else if (obj is DataTable)
    data = AdoNetHelper.SerializeTypedDataTable(obj as DataTable);
   else
   {
    throw new InvalidOperationException("Not a supported Ado.Net object");
   }
   info.AddValue("_", data);
  }

检查类型并调用 AdoNetHelper 上的正确辅助方法来创建 byte[],然后将其存储在传入的 SerializationInfo 块中。(注意第一个比较中的 IModifiedTypedDataSet 检查。添加此检查是为了支持在运行时具有附加表和/或列的类型化 DataSets(我恰好使用了一个!)。通过将它们视为普通 DataSets,它们的架构将得到保存,包括附加的表/列 - 在大多数情况下您不需要这个。)

反序列化代码如下所示:

public object SetObjectData(object obj, SerializationInfo info,
                            StreamingContext context,
                            ISurrogateSelector selector)
{
   obj = createNewInstance(obj);
   byte[] data = (byte[]) info.GetValue("_", typeof(byte[]));

   if (obj.GetType() == typeof(DataSet) || obj is IModifiedTypedDataSet)
    return AdoNetHelper.DeserializeDataSet(obj as DataSet, data);
   else if (obj.GetType() == typeof(DataTable))
    return AdoNetHelper.DeserializeDataTable(obj as DataTable, data);
   else if (obj is DataSet)
    return AdoNetHelper.DeserializeTypedDataSet(obj as DataSet, data);
   else if (obj is DataTable)
    return AdoNetHelper.DeserializeTypedDataTable(obj as DataTable, data);
   else {
    throw new InvalidOperationException("Not a supported Ado.Net object");
   }

  }

基本上它是序列化的反向操作,带有一个转折:传入的对象是完全未初始化的 - 甚至没有对其调用默认构造函数。createNewInstance 方法只是创建一个与传入对象类型相同的对象的新实例 - 这样我们就知道有一个已初始化的对象可以传递给 AdoNetHelper 反序列化方法。对于 DataSetDataTable 对象,我们直接创建一个新实例;对于其他任何对象,例如类型化 DataSets,我们使用 Activator.CreateInstance 并传入类型 - 因此需要为支持的对象提供一个无参构造函数。

这听起来很复杂,但实际上是一次性设置。之后,您就可以在不修改任何应用程序代码的情况下获得快速序列化。

4. 工作原理

序列化/反序列化代码广泛使用 BitVector32 标志,因此对于不熟悉此结构的人来说,这是一个快速的入门介绍 - 随时可以跳过。

BitVector32

BitVector32 结构是 Int32 的包装器,因此它允许以两种方式(但不能同时)存储多达 32 位的信息:按 Section 或按 Mask。

Section 使用 CreateSection 静态方法创建并定义小整数。因此,如果您需要存储一个值范围在 0 到 59 之间(例如,分钟或秒)的数字,将分配一个 6 位 Section(因为 6 位可以存储 0 到 63 之间的值)。通过调用 CreateSection 静态重载并传入前一个 BitVector32.Section 实例来“链接”进一步的 Section - 这可确保 Section 位不会重叠。布尔值不支持 Section 模式,但您可以创建一个 1 位 Section 并手动获取/设置“1”值以达到相同的结果。有关如何使用 Section 的示例,请参阅 SerializationWriter.WriteOptimized(DateTime) 方法。

Mask 模式用于存储多达 32 个布尔值。创建 Mask 需要调用 BitVector32.CreateMask 静态方法,该方法返回一个设置了单个位的 Int32。Mask 的“链接”方式与 Section 相同,方法是传递前一个 Mask/Int32 到 BitVector32.CreateMask 静态重载方法。

通常,您会将一组 Mask/位标志(我在本文中互换使用这两个术语)定义为静态只读 int,因为它们的值在运行时不会改变,然后通过将它们传递给 BitVector32 索引器来获取/设置布尔值来在方法中使用它们。

这是一个示例

private static readonly int TypeAFirstBitFlag = BitVector32.CreateMask();
private static readonly int TypeASecondBitFlag
                              = BitVector32.CreateMask(TypeAFirstBitFlag);
private static readonly int TypeAThirdBitFlag
                              = BitVector32.CreateMask(TypeASecondBitFlag);

private static readonly int TypeBFirstBitFlag = BitVector32.CreateMask();
private static readonly int TypeBSecondBitFlag
                              = BitVector32.CreateMask(TypeBFirstBitFlag);

public void MyMethod() {
  BitVector32 myFlags = new BitVector32();

  myFlags[TypeAFirstBitFlag] = myBoolValue1;
  myFlags[TypeASecondBitFlag] = true;
  myFlags[TypeAThirdBitFlag] = false;
}

注意第一个创建后的标志/Mask 的链接,并注意您为不同的对象类型创建不同的标志*集*。

使用位标志

以下是在定义位标志集时的一些技巧:

  • 明确哪个对象类型是标志所包含的信息。例如,所有 DataSet 标志都以“DataSet”开头。在这样做之前,我曾遇到过一个晦涩的问题,当时一个 DataTable 错误地使用了 DataSet 的标志 - 当时它们恰好在同一位置,因此具有相同的值,但当我“移动”了 DataSet 标志(稍后会说明原因)后,测试失败,并且原因不明显。
  • 确保传递给 CreateMask 方法的 int mask 是正确的,因为它用于创建下一个 int mask 值。如果您将相同的 mask 作为参数传递两次,您将获得两个具有相同值的 mask - 不会有编译器或运行时错误。
  • 使用可读且描述性的名称 - 频繁使用“Is”和“Has”,例如“DataSetIsCaseSensitive”或“TableHasRows”,但仅当其读起来正确时。
  • 如果在一个集合中定义了 28 个或更少的标志(可能总是如此!),请在 SerializationWriter 中使用 WriteOptimized(BitVector32) 方法,以最少的字节数(通常为 1 或 2)存储标志。
  • 按标志排序,以便最不可能设置的标志排在列表的更前面(从而生成更高的 int mask 值) - 甚至可以考虑反转某些情况下的逻辑。当有 7 个或更少的标志时,这无关紧要,它们永远不会占用超过一个字节,但当有 8 个或更多标志时可能会有所帮助。序列化流中占用的字节数取决于实际设置的最高位,而不是定义的标志数。因此,如果您有 12 个定义的标志,但最高 5 个很少设置,它们通常会占用 1 个字节,而很少占用 2 个字节。
  • 如果您需要存储单个位信息,但针对许多项,例如在一个大列表中,可以考虑使用 BitArray - 在集合内容之前将 BitArray 存储到流中,然后在主循环中通过索引器逐个访问每个位。对于两个或更多位的信息,最好坚持使用 BitVector32

位标志可以以几种方式使用

  • 直接存储目标对象的布尔数据值
  • 存储数据集合是否包含任何项(这比存储 0 的 Int32 计数值更容易、更快)
  • 存储数据值是否存在
  • 存储数据值是否与默认值不同
  • 存储数据值是否与公共值不同

列表中的最后三项听起来相似,但有细微差别。第一项通常是与 null 的简单比较“myValue != null”(或“myValue != null && myValue.Count != 0”,如果所讨论的值是惰性实例化的集合);第二项需要一些内部知识(同样,Reflector 是您的朋友);第三项是需要了解最可能值的判断。

标志不一定*直接*与数据值相关。例如,在 UniqueConstraints 标志集中,我有一个名为“UniqueConstraintHasMultipleColumns”的标志,它只是内部信息,表明是否需要存储列计数,或者我们可以假设只有一个列。

另一个例子是在 DataRelation 标志集中,我有一个“RelationIsPrimaryKeyOnParentTable”。在绝大多数情况下,这将为真,因此我可以避免为关系的那一侧保存任何列信息。

另一个很好的例子是对于 AutoIncrement 设置为 True 的 DataColumn。默认情况下,AutoIncrementSeedAutoIncrementStep 分别设置为 0 和 1。然而,我相信我不是唯一一个将这两个值都设置为 -1,以确保生成的 C# 不会与实际数据库值冲突。通过创建两个标志 ColumnHasAutoIncrementNegativeStepColumnHasAutoIncrementUnusedDefaults,我们可以有效地拥有两个“默认”值,它们可能在大多数情况下适用,从而节省存储两个长值。

还可以将一个位标志用作多个值的条件。ColumnHasAutoIncrementUnusedDefaults 是一个例子 - 如果为 false,则 AutoIncrementSeed 和 AutoIncrement 作为一对写入。

分析您的对象

我不能诚实地说我坐下来,分析了需求并一次性编写了代码 - 有很多修订和“优化机会”。我将在此处尝试提供在此和其他类似项目中使用的通用指导,这可能有助于您快速序列化自己的类。

  • 确定所有涉及的类以及它们之间的关系 - 层级结构等,这通常会提供良好的指导,但请记住,您放入/从 byte[] 中获取的内容以及它们的顺序完全取决于您。
  • 排除内部类 - 您通常无法访问它们,重要的是它们包含的数据。只要您能直接或间接获取该数据,您就能重新创建您的对象。
  • 排除临时数据,例如索引、查找 HashtablesDataViews 等。这些数据不需要重新创建对象 - 请记住,您想序列化重新创建对象所需的绝对最少数据 - 任何内部数据都可以留给对象稍后重新创建。
  • 让反序列化侧影响序列化侧的编写方式。反序列化期间可能存在一些顺序依赖性,如果您先开始编写序列化代码,这些依赖性可能不明显。一个明显的例子是 ForeignKeyConstraints,它需要先反序列化两个相关表才能创建约束。通过完全从 DataTables 中删除 ForeignKeyConstraints 的关联,我们可以确保在处理它们之前已重新创建所有表,而如果我们尝试将它们作为 DataTable 的一部分作为约束进行处理,我们会遇到问题,这取决于表添加到 DataSet 的顺序。
  • 将例程分离到私有方法中,以便它们可以重用,例如用于 ExtendedProperties 的代码在多个地方重用。此外,您可能需要序列化不同的“根”对象 - 在此项目中,序列化 DataSet 和 DataTables 的代码将使用共享代码。
  • 使用单元测试来帮助您安全地重构。我必须承认我仍然相对不熟悉单元测试,但发现它们对于这个项目来说是无价的。
  • 序列化和反序列化代码可以重构为两个单独的类,但请记住,标志需要从两者中可访问。对于这个特定的项目,我不认为需要自定义或继承,所以我选择使用内部类和静态辅助方法 - 如果您愿意,可以自由地将它们分开。

这是我开始编写代码的粗略概述

  • DataSet
    (标志/自己的数据)
  • DataTables
  • ForeignKeyConstraints
  • DataRelations
  • ExtendedProperties
  • DataTable
    (标志/自己的数据)
  • DataColumns
  • ExtendedProperties
  • DataRows
  • UniqueConstraints
  • DataColumn/DataRow/UniqueConstraints/ForeignKeyConstraints/DataRelation
    (标志/自己的数据)
  • ExtendedProperties(DataRow 除外)
  • ExtendedProperties
    (2 个 object[] 用于键和值)

下面,我将主要的类类型作为标题,并描述了它们是如何编码和优化的。

DataSet

这是序列化 DataSet 的代码:

   public byte[] Serialize(DataSet dataSet)
   {
    this.dataSet = dataSet;
    writer = new SerializationWriter();

    BitVector32 flags = GetDataSetFlags(dataSet);
    writer.WriteOptimized(flags);

    if (flags[DataSetHasName]) writer.Write(dataSet.DataSetName);
    writer.WriteOptimized(dataSet.Locale.LCID);
    if (flags[DataSetHasNamespace]) writer.Write(dataSet.Namespace);
    if (flags[DataSetHasPrefix]) writer.Write(dataSet.Prefix);
    if (flags[DataSetHasTables]) serializeTables();
    if (flags[DataSetHasForeignKeyConstraints])
        serializeForeignKeyConstraints(getForeignKeyConstraints(dataSet));
    if (flags[DataSetHasRelationships]) serializeRelationships();
    if (flags[DataSetHasExtendedProperties])
        serializeExtendedProperties(dataSet.ExtendedProperties);

    return getSerializedBytes();
   }

我不会复制代码给所有方法,但这是顶层代码,这种模式倾向于对 DataTableDataRow 等重复。

  1. 创建一个 BitVector32 并用相关标志填充当前对象。
  2. 将信息写入 SerializationWriter 实例;尽可能使用标志使其条件化。

反序列化通常是这个过程的逆过程。从 SerializationReader 读取的所有值必须与写入的顺序相同。但是,您不一定必须以相同的顺序*应用*读取的信息。此示例表明,标志首先被读取,但 DataSetAreConstraintsEnabled 值(从位标志中获取)最后应用,以防止在反序列化行数据时发生异常。

   public DataSet DeserializeDataSet(DataSet dataSet, byte[] serializedData)
   {
    this.dataSet = dataSet;
    reader = new SerializationReader(serializedData);

    dataSet.EnforceConstraints = false;

    BitVector32 flags = reader.ReadOptimizedBitVector32();

    if (flags[DataSetHasName]) dataSet.DataSetName = reader.ReadString();

    dataSet.Locale = new CultureInfo(reader.ReadOptimizedInt32());
    dataSet.CaseSensitive = flags[DataSetIsCaseSensitive];

    if (flags[DataSetHasNamespace]) dataSet.Namespace = reader.ReadString();

    if (flags[DataSetHasPrefix]) dataSet.Prefix = reader.ReadString();

    if (flags[DataSetHasTables]) deserializeTables();
    if (flags[DataSetHasForeignKeyConstraints])
        deserializeForeignKeyConstraints();

    if (flags[DataSetHasRelationships]) deserializeRelationships();
    if (flags[DataSetHasExtendedProperties])
        deserializeExtendedProperties(dataSet.ExtendedProperties);

    dataSet.EnforceConstraints = flags[DataSetAreConstraintsEnabled];

    throwIfRemainingBytes();
    return dataSet;
   }

某些对象具有隐含的顺序。例如,DataTables 首先被序列化,然后是 ForeignKeyRelationships,然后是 Relationships。由于在序列化时所有信息都可用,因此这些信息可以以任何顺序*写入*。但是,反序列化*要求*在处理 ForeignKeyConstraints 之前反序列化所有表,而 Relationships 的反序列化*要求*所有 ForeignKeyConstraints 都已就位。因此,在编写序列化代码之前考虑反序列化如何工作通常是有帮助的。

DataTable

如果我们已经达到了序列化 DataTable 的代码,那么我们知道至少有一个要保存。因此,我们首先保存计数,然后依次保存每个 DataTable 的详细信息。

DataTable 上的一些字段不可公开访问,但我们需要它们的值才能正确反序列化。CaseSensitive 和 caseSensitiveAmbient 就是例子。前者是 DataTable 上的属性,因此在反序列化时获取该值并设置它没有问题。然而,仅此一项还不够。Reflector 显示 getter 使用一个名为“caseSensitiveAmbient”的私有字段,当它被设置时,它返回包含的 DataSetCaseSensitive 属性。这允许所有 DataTables 使用相同的共享值,除非在 DataTable 上*专门*设置了 TrueFalse。如果我们未能恢复 caseSensitiveAmbient 值,那么我们所有反序列化的 DataTables 都将使用 DataSet 值,即使在 DataTable 上专门设置了值。

DataTables 也是 DataColumnsDataRowsUniqueContraints 的容器。

DataColumn

这些的序列化基本上遵循检索标志并根据标志有条件地序列化值的模式。

我们在前面的示例中已经提到了 ColumnHasAutoIncrementUnusedDefaultsColumnHasAutoIncrementNegativeStep 标志。如果前者未设置,我们将从流中读取所需的值。

DataRow

序列化首先写入行数。请记住,如果没有行(至少从 DataTable 来看),此方法根本不会被调用,但此方法也用于写入行数据的辅助方法。

DataRow 有一个 RowState 属性,它有 5 个可能的值:Added、Deleted、Detached、Modified 和 Unchanged。然而,就我们的目的而言,Detached 不是一个选项,所以我们只需要处理 4 个值。这可以通过指定其中一个可能的值(例如 Unchanged)作为默认值,然后仅在 RowState 不为 Unchanged 时存储它来实现。但是,DataRow 可能还分配了 RowError 和/或在列级别关联了 Errors。这些表明需要设置标志,所以我使用了两个附加标志 RowHasOldDataRowHasNewData - 来存储这些信息。通过使用这些标志,很容易确定需要查询哪个 DataRowVersion 来获取对象数组中的值。

  • 对于 Added 和 Unchanged 行,我们需要获取 DataRowVersion.Current 版本值;
  • 对于 Deleted 行,我们需要获取 DataRowVersion.Original 版本值;
  • 对于 Modified 行,我们需要同时获取 Current 和 Original 版本值,并使用 SerializationWriter 上的一个重载,该重载接受两个 object 数组(必须长度相同)。此优化会将第一个 object 数组正常写入,但对于第二个 object 数组,它会将其每个值与第一个数组中的相应值进行比较,并在它们相同时,仅存储 SerializedType.DuplicateValueType TypeCode。由于大多数修改过的行只更改一小部分值,因此这是一个很好的节省空间的方法。

我们本可以使用 ItemArray 属性来检索这些行版本的大部分值,但 ItemArray 对 Deleted 行会抛出异常。要获取 DeletedRow 值需要使用一个允许传入所需 DataRowVersion 的索引器重载。为了避免为 Deleted 行和所有其他状态使用一种方法,我创建了一个接受 DataRow 和 DataRowVersion 作为参数的辅助方法,并返回一个 object 数组(实际上 ItemArray 在内部也执行相同的操作 - 您知道数据实际上存储在 DataColumn 中而不是 DataRow 中吗?我不知道 - Reflector 很棒!)。由于这是一个通用方法,我将其设为公共静态方法,并将其放在 AdoNetHelper 类中。

我们还必须处理 Expression 列,以确保在序列化之前将其值设置为 null。为此,我创建了一个 int[],其中包含任何具有 Expression 的 DataColumn 的序号。这在序列化值循环之前完成,因为它只需要计算一次,一个私有辅助方法获取 DataRow 和所需的 DataRowVersion 的所有值,并且当 int[] 不为空时,将值设置为 null,这些值对应于计算列。

这是反序列化的代码

   private void deserializeRows(DataTable dataTable)
   {
    ArrayList readOnlyColumns = null;
    int rowCount = reader.ReadOptimizedInt32();

    dataTable.BeginLoadData();
    for(int i = 0; i < rowCount; i++)
    {
     BitVector32 flags = reader.ReadOptimizedBitVector32();
     DataRow row;

     if (!flags[RowHasOldData])
      row = dataTable.LoadDataRow(reader.ReadOptimizedObjectArray(),
                                 !flags[RowHasNewData]);
     else if (!flags[RowHasNewData])
     {
      row = dataTable.LoadDataRow(reader.ReadOptimizedObjectArray(), true);
      row.Delete();
     }
     else
     {
      /* LoadDataRow doesn't care about ReadOnly columns but ItemArray does
         Since only deserialization of Modified rows uses ItemArray we do this
         only if a modified row is detected and just once */
      if (readOnlyColumns == null)
      {
       readOnlyColumns = new ArrayList();
       foreach(DataColumn column in dataTable.Columns)
       {
        if (column.ReadOnly && column.Expression.Length == 0)
        {
         readOnlyColumns.Add(column);
         column.ReadOnly = false;
        }
       }
      }

      object[] currentValues;
      object[] originalValues;
      reader.ReadOptimizedObjectArrayPair(out currentValues,
                                          out originalValues);
      row = dataTable.LoadDataRow(originalValues, true);
      row.ItemArray = currentValues;
     }

     if (flags[RowHasRowError]) row.RowError = reader.ReadString();
     if (flags[RowHasColumnErrors])
     {
      int columnsInErrorCount = reader.ReadOptimizedInt32();
      for(int j = 0; j < columnsInErrorCount; j++)
      {
       row.SetColumnError(reader.ReadOptimizedInt32(), reader.ReadString());
      }
     }

    }

    // Must restore ReadOnly columns if any were found when deserializing a
    // Modified row
    if (readOnlyColumns != null && readOnlyColumns.Count != 0)
    {
     foreach(DataColumn column in readOnlyColumns)
     {
      column.ReadOnly = true;
     }
    }

    dataTable.EndLoadData();

   }

包含 DataRowVersion 参数的 DataRow 上的索引器是只读的,因此我们需要一种替代方法将 object 数组放回。LoadDataRow 是最快的方法,尤其是当我们使用 BeginLoadData 来包装该过程时,它告诉 DataTable 期望添加大量数据,并在调用 EndLoadData 之前暂停索引/约束检查。

对于 Unchanged 和 Added 行,我们只需提供反序列化的 object 数组和一个布尔值(从我们的标志集中获取),以指定是立即调用 AcceptChanges 还是不调用(Unchanged 为 true,Added 为 false。DeletedRows 类似,但我们先接受,然后立即调用 Delete() 以达到所需效果。

Modified 行的处理方式略有不同,因为我们有两组 object 数组,但 LoadDataRow 只能接受第一组 - 之后我们需要将第二组应用于现有数据以“修改它”。

ItemArray 属性将允许我们执行此操作,但有一个陷阱 - 对于 ReadOnly 列会抛出异常。为了解决这个问题,我们需要在反序列化数据时删除所有列(表达式列除外)的 ReadOnly 状态。

为了优化这一点,我们只在找到 Modified 行时才执行此操作,并且只执行一次,并记录我们更改的列序号 - 在反序列化所有数据后,我们会将这些列再次设置为 ReadOnly。

UniqueConstraint

UniqueConstraint 总是有一个名称 - 它永远不是 null 或为空。如果您没有指定特定名称,它将默认为“Constraintxx”,其中 xx 是 DataSet 中的下一个数字。

我们可以利用这一点 - 获取与 UniqueConstraint 相关的标志的方法使用正则表达式查找默认名称。如果找到,我们只需要序列化 xx 数字;否则,我们存储完整指定的约束名称。

Unique Constraint 最终需要在反序列化时存储 DataColumns。有几种方法可以做到这一点 - 我们可以存储列名,并且由于它们在 DataColumn 序列化过程中已经存储,它们只会占用字符串令牌的大小。但是,我们可以做得更好 - 由于我们知道在反序列化 UniqueConstraints 时,DataTable 及其 DataColumns 已经恢复,我们只需要存储 DataColumn 的序号,然后从 DataTableDataColumnCollection 中查找它们 - 每个列只有一个字节(除非您有一个序号大于 127 的 DataColumn)。

此外,通过使用一个指示约束是否由多个 DataColumn 组成的位标志,我们无需在单个列约束的一般情况下存储列计数。

ForeignKeyConstraint

ForeignKeyConstraint 需要存储两组 DataTable/DataColumn 组合。我们知道涉及的列数对于两者来说总是相同的,我们还可以使用与 UniqueConstraint 相同的 ForeignKeyConstraintHasMultipleColumns 标志优化来节省在已知为一次的情况下存储列计数。

虽然 ForeignKeyConstraint 位于子 DataTableConstraintCollection 中,但我们将其*在 DataTable 之外*序列化,因为我们希望消除反序列化期间的顺序依赖性,而且我们也不希望序列化 DataTable 独自序列化时的 ForeignKeyConstraints

因此,我们需要识别涉及的表,我们可以通过使用 DataTableDataSet Tables 集合中的序号来做到这一点 - 只有一个字节。之后,我们对 DataColumn 信息执行相同的操作 - 一个可选的列计数,然后是一个或多个 DataColumn 序号的列表。

通常,父端将是 DataTablePrimaryKey。我们可以检查这一点并使用一个位标志,这样,对于父 DataTable,我们甚至不需要存储 DataColumn 序号 - 只需一个字节来标识表。

ForeignKeyConstraint 也有规则:AcceptRejectRuleUpdateRuleDeleteRule

对于后两者,选项是 NoneCascadeSetNullSetDefault。由于默认值为 Cascade,我们可以反转位标志的含义并将它们命名为 ForeignKeyConstraintHasNonCascadeUpdateRuleForeignKeyConstraintHasNonCascadeDeleteRule。通过这样做,更有可能不设置这些位,并且由于 ForeignKeyConstraint 的可能标志数恰好是存储为一个或两个字节的临界点,它将平衡倾向于单个字节。(是的,这对于典型的 DataSet 来说只会节省几个字节,但这种类型的优化和任何其他类型的优化一样容易,只执行一次,并且原则是健全且可扩展的 - 如果对存储多次的类型执行此操作,节省将更加明显。)

DataRelation

DataRelation 对 DataTable/DataColumn 存储使用与 ForeignKeyConstraint 相同的优化技术。

ExtendedProperties

这是 PropertyCollection 的一个实例,根据文档,它在 DataSetDataTableDataColumn 对象上可用。它实际上也存在于 Constraints 和 Relationships 上。标准的 DataSet 序列化仅在 XML 中序列化字符串值,但我们可以做得更好,并序列化其他对象。

PropertyCollection 派生自 Hashtable(实际上它什么都没有添加),所以我们只需要创建一个键的对象数组和一个值对象数组并存储它们。由于这是上述所有类型的常见要求,因此我们有一个单独的方法,它接受任何 PropertyCollection 作为参数并序列化其内容。请记住,如果 PropertyCollection 为空,该方法甚至不会被调用。

最后的话和注意事项

关于快速序列化,我应该指出的一个注意事项是,它最初是为远程处理目的设计的。由于远程处理将在任何给定时间涉及在序列化和反序列化两端使用相同的代码,因此它对代码的任何修改都是免疫的。然而,如果您将序列化数据持久化到文件或数据库,情况就不一定如此了 - 标志或存储顺序的任何更改都可能使数据不可读。

标准序列化在一定程度上也有这个问题,但可以通过检查 SerializationInfo 中的字符串键来确定值是否存在并采取相应措施,尽管会很混乱。

所以您可以选择仅将代码用于远程处理;假设代码是无 bug 的并且永远不会改变;或者在 SerializationInfo 块中添加一个额外的版本号,并准备在需要时创建特定于版本的代码。

希望您发现此文章和上一篇文章中的代码对您的序列化/远程处理需求有用。

请随时在此 Code Project 上发表评论、建议任何更改或报告任何 bug。

v1 到 v2 的更改

  • 使用条件编译添加了对 .NET 2.0 的支持。您可以将“NET20”添加到项目属性(“生成”选项卡下的“常规”)的条件编译符号中,或者搜索“#if NET20”并手动删除不需要的代码和条件构造。
  • 更新了 FastSerializer 代码以支持 NET 2.0(源代码也包含在此文章的下载中,但请参阅第一篇文章中的“历史记录”部分以获取所有更改的完整详细信息)。简要亮点是:
    • 添加了对 NET 2.0 日期的支持。
    • 添加了对类型化数组的支持。
    • 添加了对可空泛型类型的支持。
    • 添加了对 List<T>Dictionary<K,V> 泛型类型的支持。
    • 添加了对可选数据压缩的支持。
  • 更新了 AdoNetHelper 代码以支持 NET 2.0。
    • DataTable 的文化和大小写敏感性在 .NET 2.0 日期中已更改。
  • 两个小 bug 修复 - 感谢 Ales Helbelka 发现这些。

历史

  • 2006-10-31 v1.0 在 CodeProject 上发布。
  • 2006-11-27 v2.0 在 CodeProject 上发布。
    • FastSerializer.cs
      • 添加了对 .NET 2.0 和可选的实时压缩的支持(有关所有更改的完整详细信息,请参阅第一篇文章)。
    • AdoNetHelper.cs
      • DataTable 的文化和大小写敏感性添加了 .NET 2.0 条件代码。
      • 为了保持一致性,使用了 WriteOptimized(string)
      • 修复:添加了新的位标志和用于 DataColumn 的代码,其中 MaxLength = int.MaxValue - 感谢 Ales Helbelka。
      • 修复:在 GetRelationFlags 中添加了检查以确保 ParentKeyConstraint 不为 null - 感谢 Ales Helbelka。
    • AdoNetHelperTests.cs
      • DataTable 的文化和大小写敏感性添加了 .NET 2.0 条件代码。
      • 更新了预期的序列化大小。
      • 添加了 DataColumn.MaxLength = int.MaxValue 的测试。
      • 添加了无主键 DataRelation 的测试。
    • FastSerializableDataSet.cs
      • GetObjectData() 方法添加了 .NET 2.0 条件代码,因为该方法现在是虚拟的。
© . All rights reserved.