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

一个深度 XmlSerializer,支持复杂类、枚举、结构体、集合和数组

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (37投票s)

2006年9月20日

CPOL

16分钟阅读

viewsIcon

312807

downloadIcon

3112

一个深度 XmlSerializer,支持复杂类、枚举、结构体、集合、泛型和数组

引言和背景

在软件开发中,序列化应用程序/配置文件数据或对象状态并在时间和空间上重构是最常见的任务之一。有时,要序列化的数据结构很简单,有时则非常复杂。

除了专有格式和机制之外,.NET Framework 主要提供了三个类来序列化和反序列化对象:

  • XmlSerializer System.Xml.Serialization 命名空间)
  • BinaryFormatter System.Runtime.Serialization.Formatters.Binary 命名空间)
  • SoapFormatter System.Runtime.Serialization.Formatters.Soap 命名空间)

不幸的是,它们都有局限性。例如,XmlSerializer 不支持集合,BinaryFormatterSoapFormatter 只序列化标记为可序列化的类。此外,使用 BinaryFormatter 序列化的对象无法手动编辑(有时这是期望的)。另一个方面是能够将一组对象序列化到同一个文档中。

CodeProject 上 Marc Clifton 的文章《“一个简单的序列化/反序列化器”》启发了我编写一个满足我特定需求的深度 XML 序列化和反序列化器。

本文针对深度 XML 序列化,支持复杂类、枚举、结构体、集合、数组、泛型类型、二进制数据以及更多内容。本文描述的 XmlSerializerXmlDeserializer 类“不声称是完整或完美的”!相反,它们试图展示一种解决深度 XML 序列化问题的其他方法。

Using the Code

XmlSerializerXmlDeserializer 基于反射,包括递归调用。简而言之,工作原理如下:遍历对象的属性,确定其值、类型名称和程序集名称,并将它们写入 XmlNode(公共字段不考虑)。复杂类型的属性以相同的方式处理,并嵌套在其父类的 XmlNode 中。集合和数组也逐项遍历,考虑每个项的类型(复杂、集合、数组)。请跟随代码,其中包含注释。

XML 格式

我决定根据每个元素的角色来命名。属性集合称为 properties,单个属性称为 property。集合的元素称为 item,并嵌套在 items 元素中。序列化对象的根元素是 object 元素。每个 propertyitem 元素都包含 typeassembly 属性,用于反序列化目的描述其 Type。属性的名称属性称为 name,属性或项的 value 是元素的值。以下示例将让您对结构有所印象(请注意,程序集属性已简化 - 留空)

<object name="" type="XmlSerializerDemo.ComplexDummyClass" assembly="">
  <properties>
    <property name="Name" type="System.String"
              assembly="">CodeProject</property>
    <property name="Number" type="System.Int32"
              assembly="">1234</property>
    <property name="Value"
              type="System.Collections.Hashtable" assembly="">
      <items>
        <item>
          <properties>
            <property name="Key" type="System.String"
                      assembly="">my super key</property>
            <property name="Value" type="System.Double"
                      assembly="">100.4512</property>
          </properties>
        </item>
        <item>
          <properties>
            <property name="Key" type="System.String"
                      assembly="">Klaus</property>
            <property name="Value" type="System.Int32"
                      assembly="">1234</property>
          </properties>
        </item>
      </items>
    <property>
  </properties>
</object>

XML 标签命名

由于有些人可能不同意命名约定,因此增加了一些灵活性:通过实现自定义的 IXmlSerializationTag 实现,可以任意命名 XML 节点。如果您编写了自定义的 IXmlSerializationTag 实现,只需将该实现的一个实例设置到 XmlSerializerXmlDeserializerTagLib 属性即可。
注意:序列化和反序列化时务必使用相同的 IXmlSerializationTag 实现!

由于此功能并非绝对必要,并且很可能引起问题,因此不应使用它。出于某种原因,它被实现了。

类型字典

如上例所示,类型信息(TypeAssembly)会为每个属性及其值存储。当然,这会影响文件大小。想象一个要序列化的 10,000 项的 String 数组:相同的类型和程序集信息将被写入 10,000 次。显然,这不理想。解决方案是使用类型字典。

在序列化过程中,找到的每个 Type(在内部)都会被添加到字典中并获得一个唯一的键。该键将写入属性 XmlNodetype 属性,而不是完整的类型信息。assembly 属性留空。处理完所有属性后,此类型字典将附加到对象的根节点。

此类型字典的使用是 XmlSerializer 的一个可选但推荐的属性。

在反序列化时,XmlDeserializer 会检查对象的根节点是否包含类型字典,如果是,则首先反序列化类型字典。在反序列化过程中,它会尝试从字典中解析属性的 Type。如果失败,它会尝试直接从 typeassembly 属性解析 Type

类型信息存储在 TypeInfo 对象中,这些对象保存类型名称以及程序集名称。类型字典的序列化方式与 objects 属性相同。以下示例显示了简化的结构。

<object name="" type="TK0" assembly="">
  <items>
    <item name="0" type="TK1" assembly="">Item 0</item>
    <item name="1" type="TK1" assembly="">Item 1</item>

     <!-- To be continued... -->

    <item name="9999" type="TK1" assembly="">Item 9999</item>
  </items>

  <typedictionary name="" type="System.Collections.Hashtable" assembly="">
    <items>

      <item>
        <properties>
          <property name="Key"
                    type="System.String"
                    assembly="">TK0</property>
          <property name="Value"
                    type="Yaowi.Common.Serialization.TypeInfo"
                    assembly="">
             <properties>

               <!-- The type information are stored here -->

             </properties>
          </property>
        </properties>
      </item>

      <item>
        <properties>
          <property name="Key"
                    type="System.String"
                    assembly="">TK1</property>
          <property name="Value"
                    type="Yaowi.Common.Serialization.TypeInfo"
                    assembly="">
             <properties>

                <!-- The type information are stored here -->

             </properties>
          </property>
        </properties>
      </item>

    </items>
  </typedictionary>

</object>

在我的测试中,使用类型字典可以将文件大小减小多达 50%。

类型字典的使用是可选的,可以通过 XmlSerializerUseTypeDictionary 属性进行设置。默认设置为 true

循环引用

循环引用很烦人,并且可能导致无限循环。不幸的是,它们出现的频率并不低。只需考虑 System.Windows.Forms 容器控件(例如 Form)及其 Controls 集合属性,该属性包含对所有嵌套控件的引用。这是一个方向。反方向是控件的 Parent 属性。父控件引用其子控件,子控件也引用其父控件。

遍历所有属性时,您会多次遇到同一个实例并一遍又一遍地处理它。为了避免在序列化过程中出现这些循环引用,会构建一个集合来存储所有已处理的实例。在处理属性的值之前,XmlSerializer 会检查该实例是否之前已被处理过。如果是,XmlSerializer 将跳过该属性。

此过程可确保不会出现无限循环,但因此,XmlDeserializer 也无法重建循环引用(演示应用程序为此行为提供了示例)。
我必须承认这还没有完全解决。解决这个问题是未来的任务。

注意:XmlSerializer 实现 IDisposable 接口以清除实例集合。建议确保调用 Dispose()

Generics

在本文章的早期版本中,我曾说过泛型类型不受支持。事实并非如此。泛型类型可以被序列化和反序列化。但它们必须实现 ISerializableIObjectReference 接口(大多数 .NET 泛型类型都实现了)。

演示包含了一个关于如何在自定义泛型类型中实现这些接口的示例(请注意 Serializable 属性)。

[Serializable]
public class GenericDummyClass<T> : ISerializable, IObjectReference
{
  private T instanceoftype;

  // Example property
  public T InstanceOfType
  {
    get { return instanceoftype; }
    set { instanceoftype = value; }
  }

  // ISerializable member
  public void GetObjectData(SerializationInfo info, StreamingContext context)
  {
    info.SetType(typeof(T));
  }

  // IObjectReference member
  public object GetRealObject(StreamingContext context)
  {
    return this.instanceoftype;
  }
}

二进制数据

属性可能包含二进制数据,或者要序列化的对象本身就是二进制格式。显而易见,二进制数据必须以不同的方式处理。但是,如何确定一个 Type 包含需要二进制序列化的数据?例如,一个 ImageBitmap)有一些属性,但至少它必须以二进制格式序列化才能正确恢复。那么,Bitmap 的二进制序列化的指示是什么?
我决定通过检查是否存在一个带有确切一个 byte[]Stream 类型参数的构造函数来确定,第二个指示是,对象的 TypeConverter 可以转换为 byte[]Stream。以 Bitmap 为例,它具有构造函数 Bitmap(Stream),其 TypeConverterSystem.Drawing.ImageConverter)可以转换为 byte[]。这应该足以证明该类型的实例可以作为 byte[] 进行二进制序列化和反序列化。

被识别为二进制的对象不会以常见的 <property><properties><property>... 方式进行序列化。取而代之的是,它们会获得一个 <constructor> 标签,其中包含子 <binarydata>,它保存以 base64 数字编码的 byte[] 数据(加上强制的类型信息属性)。其他属性将被忽略,假定它们包含在二进制表示中。

为了序列化任意二进制数据,引入了 BinaryContainerBinaryContainerTypeConverter 类。BinaryContainer 作为二进制数据的容器(顾名思义),而 BinaryContainerTypeConverter 是相应的 TypeConverter 实现,以满足上述需求。

BinaryContainerTypeConverter 可以与 byte[]Stream 进行相互转换。

演示展示了如何将任意二进制文件加载到 BinaryContainer 中,以及如何将其转换为 byte[]Stream。在演示中,这是一个 JPEG 示例,但它也可以是 MP3、MPG 或其他任何文件。

// Loading a binary file and wrap it into a BinaryContainer
FileStream fs = new FileStream("MH_Wallpaper.jpg", FileMode.Open);
BinaryContainer bc = new BinaryContainer(fs);

// Do something with the BinaryContainer: serialize it to disk,
// chase it through the net...
// Eventually, deserialize it to the instance "bc"

// Get the BinaryContainerTypeConverter
TypeConverter tc = TypeDescriptor.GetConverter(bc.GetType());

// Convert the wrapped data
Stream stream = tc.ConvertTo(bc, typeof(Stream));
byte[] barr = ConvertTo(bc, typeof(byte[]));

// E.g. a Bitmap
Bitmap bmp = new Bitmap(stream);

XML 序列化不仅仅是为了序列化到磁盘,XML 也是一种转发机制。例如,让您的照片或 MP3 收藏集通过 Web 服务访问(即使您产生的流量不会让每个人都高兴,因为在我测试中,文件大小惊人地增加了多达 70%)。

程序集版本

不时地会发布新版本的程序集。序列化还意味着您可以将对象无限期地存储在磁盘上,或者可以将对象分发到另一台计算机。因此,您永远无法确定在反序列化时,序列化对象时使用的程序集版本是否(仍然或已经)存在。因此,对要加载的程序集的精确定义可能会导致错误,如果该程序集版本不存在的话。

例如,一个 Color 被序列化时使用了程序集信息

System.Drawing, Version=1.0.5000.0, Culture=neutral,
                PublicKeyToken=b03f5f7f11d50a3a 

而在反序列化时,已经有一个更新的程序集可用

System.Drawing, Version=2.0.0.0, Culture=neutral,
                PublicKeyToken=b03f5f7f11d50a3a 

...反序列化将失败并出现 FileNotFound 异常,因为找不到指定的程序集版本。

如果在上面的示例中,程序集信息被简化为仅程序集名称(System.Drawing),则反序列化会成功。因此,解决方案可能是简化用于反序列化的信息。不幸的是,还有另一种情况禁止这种方法:.NET 允许安装同一程序集的多个版本。这意味着在反序列化时,可能存在多个程序集版本。在这种情况下,必须精确指定要加载的程序集(版本、区域性、公钥令牌),否则将抛出异常。

据我所知,只有一种方法可以解决这个问题:试错!因此,XmlDeserializer 首先尝试使用简化的信息加载程序集,如果失败,则尝试使用完整的程序集信息加载。如果没有匹配的版本,则无法加载程序集。至少一个属性的反序列化将失败。

当然,这种方法并不令人满意,并且会消耗一些性能(通过在反序列化过程中缓存已加载的程序集来最小化这些成本)。但到目前为止,这是我找到的几乎所有情况都能奏效的唯一方法。

类版本

有时类需要修改,属性被添加或删除。因此,可能会发生序列化时的 Type 在反序列化时与可用属性不匹配的情况。除了 SoapFormatter,上述所有序列化器都能容忍这种情况。XmlDeserialzer 也能容忍,因为它使用 Reflection 来确定可用属性。

SoapFormatter 会抛出异常,提示“Wrong number of Members or Member name 'xyz' not found”,但可能可以通过设置 SurrogateSelector 属性来处理这种情况(我没有测试过 - 似乎需要一些编码)。

未知程序集

当程序集在运行时从文件加载(Assembly.LoadFrom("..."))并且其包含类型的实例被序列化时,不一定能保证在反序列化时自动加载该程序集。只需考虑运行时加载的插件,并且未来可能加载哪些程序集是未知的。应用程序本身对程序集没有引用。

XmlDeserialzer 提供了一个 RegisterAssembly 方法,允许将程序集注册到内部程序集缓存。通过这种机制,运行时引用的程序集可以被反序列化使用。对于插件示例:只需注册所有找到的插件程序集。

容差/调整

XmlSerializerXmlDeserializer 的容差和调整可以通过以下属性来影响:

  • XmlSerializer.IgnoreSerializableAttribute - Getset 是否忽略 ISerializable 属性。如果设置为 false,则只序列化标记为可序列化的属性。否则,甚至“不可序列化”的属性也会被序列化。默认为 false
  • XmlSerializer.SerializationIgnoredAttributeType - Getset 应用于属性时会禁用其序列化的 Attribute 类型。如果为 null,则序列化所有属性。
  • XmlSerializer.IgnoreSerialisationErrors - Getset 在序列化期间是否忽略错误。
  • XmlSerializer.TagLib - Getset XML 标签的字典。如果未显式设置,则使用默认实现。
  • XmlSerializer.UseTypeDictionary - Getset 是否使用类型字典来存储类型信息(参见上文)。
  • XmlDeserializer.IgnoreCreationErrors - Getset 是否忽略创建错误。创建错误可能发生,例如,如果类型没有无参数构造函数,并且无法从 String 实例化一个实例。或者,仅仅是找不到程序集。默认为 false
  • XmlDeserializer.TagLib - Getset XML 标签的字典。如果未显式设置,则使用默认实现。

限制

在评估和测试过程中,我发现 System.Collections.Specialized 命名空间中的某些类型会导致问题。虽然该命名空间中的大多数集合可以顺利地序列化和反序列化,但其中两个表现不同。

尝试序列化实例...

  • NameValueCollection ...什么也没发生
  • StringDictionary ...将抛出异常

由于这些类继承自没有泛型的时间,因此建议使用 Dictionary<String, String> 类型替换它们(即使 NameValueCollection 在添加重复键时也有这种奇怪的行为;我对这个功能从未真正信服)。我无意为解决这种不当行为而做出任何努力。

循环引用无法重建!解决方案可能是注册引用属性并进行序列化/反序列化。也许很简单,但需要大量工作。

据我所知,这些是唯一的限制,但如果您发现其他限制,请随时告知我。

缺点/优点

由于大量使用 Reflection,本文的解决方案并不提供最高的性能。SoapFormatterBinaryFormatter 提供显著更高的性能。XmlDeserializer 生成的文件在小型序列化操作时更大。不考虑生成最小输出(低至 10%?)的 BinaryFormatterXmlDeserializer 生成的文件对于“小型对象”(“具有 1,000 个项的 String Array”测试的 62/36 KB)来说,似乎比 SoapFormatter 生成的文件大 100%。但这只是相对而言:“Hashtable (String, SimpleDummyClass) 具有 1,000 个项”的测试显示了 509/500 KB 的比例。几乎相等。

除了尚未实现的重建循环引用的功能和(故意)未实现的字段序列化功能之外,XmlSerializerXmlDeserializer 提供了其他序列化器几乎所有功能的组合。再加上一些其他功能,如未知程序集或将序列化对象合并到单个文档中。

使用类

要将任意对象序列化到 XML 文件,需要两行代码:

ComplexDummyClass dummy = new ComplexDummyClass();
// Sample class from the demo

// ... Set some properties
XmlSerializer xs = new XmlSerializer();
xs.Serialize(dummy, filename);

要从 XML 文件反序列化任意对象,也只需要两行代码:

XmlDeserializer xd = new XmlDeserializer();
Object obj = xd.Deserialize(filename);

如果您确定对象的 Type 是什么:

XmlDeserializer xd = new XmlDeserializer();
ComplexDummyClass dummy = (ComplexDummyClass)xd.Deserialize(filename);

直接将对象保存到磁盘只是序列化方法的一个重载。其他重载允许将对象的 XML 元素追加到现有的 XmlDocumentXmlNode 中。

以下示例展示了如何将对象的 XML 元素追加到 XmlNode

ComplexDummyClass dummy = new ComplexDummyClass();

// ...

XmlDocument doc = new XmlDocument();
XmlNode root = doc.CreateElement("configuration");
doc.AppendChild(root);

XmlSerializer xs = new XmlSerializer();
xs.Serialize(dummy, "dummyconfig", root);

此方法允许您将多个对象存储在同一个 XmlDocument 中。例如,您可以在一个文件中保存数据库和 GUI 设置,以便在应用程序启动时类型安全地、严格分离地从该文件中重新加载它们。

Deserialize 方法还提供了各种重载,用于从 XmlNodeXmlDocument 进行反序列化。

此处演示了如何在运行时注册程序集。

XmlDeserializer xd = new XmlDeserializer();

Assembly a = Assembly.LoadFrom(@"..\..\DummyExternalAssembly.dll");
xd.RegisterAssembly(a);

Object obj = xd.Deserialize(filename);

要从序列化中排除属性,请为该属性定义一个(任意)Attribute,并将该 AttributeType 设置为 XmlSerializerSerializationIgnoredAttributeType 属性:

public class SimpleDummyClass
{
  // Define an Attribute
  [XmlIgnore]
  public String Name
  {
    get { return this.name; }
    set { this.name = value; }
  }
}

...

XmlSerializer xd = new XmlSerializer();

// Assign the Attribute to the XmlSerializers property
xd.SerializationIgnoredAttributeType = typeof(XmlIgnoreAttribute);

现在,序列化将忽略 SimpleDummyClassName 属性。
注意:XmlSerializerSerializationIgnoredAttributeType 接受所有类型的属性,但如果指定的 Type 不是 Attribute,则此设置将被忽略。

演示应用程序

示例应用程序允许您创建示例类 SimpleDummyClassComplexDummyClassComplexDummyClassExt 的实例。ComplexDummyClassExt 扩展了 ComplexDummyClass,并添加了一个 GraphicsPath 属性,该属性未标记为可序列化(导致在序列化时出现 BinaryFormatter 异常),此外还包含循环引用、数组、Hashtable 和泛型的示例。创建的实例将设置到 PropertyGrid 中以显示其属性。我没有花精力在 PropertyGrid 中显示集合值,但 CodeProject 上有一些文章解释了如何做到这一点。

一个 TabControl 包含用于序列化 PropertyGrid 所选对象以及从文件反序列化所有已提及序列化器的对象的命令。

要序列化和反序列化的类型 ComplexDummyClassExt(项 EXTENDED ComplexDummyClass)是最终的复杂类,包含各种示例属性。这些属性是:

  • ArrayList(包含不同类型的项)
  • System.Windows.Forms.ArrowDirection
  • DecimalNumber(Decimal)
  • IDictionary(在运行时设置为泛型字典)
  • Color
  • GenericDummyClass<String>(一个自定义泛型类)
  • GraphicsPath(未标记为可序列化)
  • Hashtable(包含不同类型的项)
  • HybridDictionary(在 Collections.Specialized 命名空间中)
  • Image(二进制)
  • Name(String)
  • Number(int)
  • ObjectArray(object[])
  • SimpleDummyClass(一个简单的自定义类)
  • Stream(二进制,从文件加载的 Bitmap)

程序集 Demo\XmlSerializerDemo\XmlSerializerDemo\DummyExternalAssembly.dll 包含一个 ExternalDummy 类(未提供代码),用于演示在运行时加载的未知程序集。

为了演示修改后的类,SimpleDummyClass 类准备了一个 AdditionalProperty 属性。要修改此类,只需在序列化和反序列化之间注释掉或取消注释此属性。

如何序列化任意二进制文件并将其包装在 XmlBinaryContainer 中,演示了一个二进制文件(包装在 XmlBinaryContainer 中)。

control 也可以被序列化(项 Control),但请注意,Control 包含 Controls 属性,该属性是只读的,因此不会被序列化。这就是为什么 Windows.Forms(继承自 Control)无法与其控件一起被序列化的原因。

在许多情况下,标准序列化机制会抛出异常,因为并非所有类型的对象都可以被所有序列化器序列化或反序列化。但这可能是对各种序列化器有用的功能比较。

演示应用程序使用我的 GenericTypeConverter<T> 类来显示 PropertyGrid 中嵌套的类。由于这个类不是本文的主题,所以只提及而不解释(可能值得写一篇短文……以后!)。

SimpleDummyClass 有一个类型为 SimpleDummyClassDummy 属性,应用了 XmlIgnore 属性。通过选中“Set Ignore Attribute”复选框,该属性将设置为 XmlSerializerSerializationIgnoredAttributeType 属性,因此该属性不会被序列化。

关注点

重建循环引用:问题尚未解决。解决方案可能是序列化引用属性的索引。有什么建议吗?

我想为我给序列化器起的名字 XmlSerializer “道歉”。尽管与 .NET 类名称重叠,但我决定以一种表达其目的的方式来命名它。在一个拥有数千个类的环境中,很快就不可能找到唯一且有意义的名称了。.NET 支持导入别名,因此避免冲突应该相当容易。在演示应用程序中,我使用了以下别名:

using SYSTEMXML = System.Xml.Serialization;

...

SYSTEMXML.XmlSerializer serializer = new SYSTEMXML.XmlSerializer();

历史

  • 2006-09-20 - 首次发布
  • 2006-10-04 - 修复了一个 bug(如果对象是集合...)。扩展了演示。
  • 2006-10-12 - XmlSerializerXmlDerserializer 的一些更改
    • XmlSerializer:避免了循环调用
    • XmlSerializer:实现了 IDisposable 接口
    • XmlSerializer:实现了可选的 Serializable 属性忽略
    • XmlDerserializer:实现了可选的创建错误忽略
  • 2006-10-20 - 主要增强!
    • 可选类型字典
    • XmlDerserializer:程序集版本独立性
    • XmlDerserializer:实现了 IDisposable 接口
    • XmlDerserializer:已加载程序集的缓存
    • XmlDerserializer:注册自定义(未知)程序集
    • 扩展了演示
    • 文章已更新以匹配增强功能
  • 2007-04-01 - 代码增强,文章更新
    • 引入了 IXmlSerializationTag
    • XmlSerializerXmlDerserializer:添加了 TagLib 属性
    • 扩展了演示(特别是泛型)
    • 文章根据新的见解(例如泛型、Collections.Specialized)进行了更新
  • 2007-04-12 - 支持二进制数据,文章修订和更新
    • 引入了 BinaryContainerBinaryContainerTypeConverter
  • 2007-12-10 - 通过 Attribute 排除属性,一些代码改进
    • XmlSerializer:添加了 SerializationIgnoredAttributeType 属性
© . All rights reserved.