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

如何正确实现 IXmlSerializable

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.79/5 (40投票s)

2009年10月22日

BSD

9分钟阅读

viewsIcon

316910

描述了实现 IXmlSerializable (.NET) 的指南和陷阱

引言

是的,我知道,这又是一篇关于 XML 序列化的文章……在 CodeProject 上看到几篇关于 XML 序列化使用或演示的文章存在问题(我自己也曾为这些问题挣扎过!),我想将我的发现告诉社区会是一件好事。看到大家对此感兴趣,我又添加了一些源代码示例。

关于 IXmlSerializable 接口的实现,有很多令人困惑的地方。即使是 MSDN(截至撰写本文时:2009年10月21日)也通过发布过于简单的示例代码来增加困惑,而且这些示例代码的实现方式甚至是错误的(请参见 此处ReadXml WriteXml 作为入门,它们可以工作但实际上是错误的,读完本文后你也许会相信我)。许多问题油然而生,我花了一段时间才找到答案。这就是本文的由来。

背景

IXmlSerializable 由三个方法组成

  • GetSchema
  • ReadXml
  • WriteXml

从 XML 序列化属性创建的序列化器,首先会查看要序列化的类型是否实现了此接口。如果未实现,则会分析(或不考虑,感谢 XmlIgnoreAttributepublic 成员和属性进行序列化。

是一个很好的入门。文章清晰且写得很好,介绍了基于属性的序列化与实现 IXmlSerializable 之间的主要区别。 IXmlSerializable.aspx 也值得一读。

在阅读完本文后,回头看上面提到的其他文章,我希望你能够发现其中实现的错误。代码在类不被扩展且不混合序列化过程的情况下工作良好。起初我也犯了很多错误,直到我深入研究了这些问题……

本文或多或少写成 FAQ 的形式,作为快速参考。它应该能回答人们可能(或应该)问过的关于实现 IXmlSerializable 的最重要问题。如果您有更多问题,请随时与我联系。我使用 C# 作为编程语言。我已尽力避免过多提及语言,实际上这些信息对所有 .NET 目标语言都很有用。

示例

为了更好地支持解释,我引入了一个包含 XML 序列化中可能遇到的许多陷阱的示例。我们想序列化和反序列化存储在农场中的动物集合。比 foos 和 bars 之类的更有趣,不是吗?

包含以下方面

  1. XML 中的空元素
  2. 要序列化的集合接口
  3. 集合包含来自基类的不同类型的元素

public abstract class Animal
{
	public Animal() { }
	public String Name { get; set; }
	public DateTime Birthday { get; set; }
}
public class Dog : Animal
{
	public Dog() { }
}
public class Cat : Animal
{
	public Cat() { }
}
public class Mouse : Animal
{
	public Mouse() { }
}
public class Farm
{
	public Farm() { Animals = new List<Animal>(); }
	public IList<Animal> Animals { get; private set; }
}

XML 片段

<Farm>
  <Dog Name="Rex">
    <Birthday>2009-10-22</Birthday>
  </Dog>
  <Cat Name="Tom">
    <Birthday>1940-06-15</Birthday>
  </Cat>
  <Mouse Name="Jerry" />
</Farm>

GetSchema() 真的应该总是返回 Null 吗?

是的!GetSchema() 应该始终返回 null。在大多数情况下这已经足够了。如果您确实需要提供一个 Schema,请使用 XmlSchemaProviderAttributeGetSchema() 可能仍被一些旧代码或 .NET 内部使用,但您不应该使用它。返回 null 是安全且良好的。告诉您实现它很重要的人都是骗子!:-)

如何实现 WriteXml?

这部分很简单,相当直接

  1. 写出所有属性
  2. 写出所有元素和子对象

但不要写包装元素!那是调用代码的工作。

就我们的例子而言,这意味着 Dog 类应该编写 "Name" 属性,然后是其 "Birthday" 元素。但是 Dog 类不应该编写 "Dog" 开始标签或结束标签。

此代码演示了如何在 WriteXml 中正确处理所有动物

public void WriteXml(System.Xml.XmlWriter writer)
{
	writer.WriteAttributeString("Name", Name);
	if (Birthday != DateTime.MinValue)
		writer.WriteElementString("Birthday",
			Birthday.ToString("yyyy-MM-dd"));
}

如何实现 ReadXml?

ReadXml 应该先读取属性,然后通过调用 ReadStartElement() 来消耗包装元素。消耗包装元素的结束标签也应该在 ReadXml 内部通过调用 ReadEndElement() 来完成。这听起来有些反直觉,因为 WriteXml 不应该写包装元素!但考虑到读取属性时,在消耗它们定义的开始标签之前才能读取属性,并且你需要从类外部知道元素名称来创建正确类型的类,这一点就更清楚了。注意:小心空元素!(见下文。)

就我们的例子而言,这意味着 Dog 类应该移动到内容并读取 "Name" 属性。然后它应该读取开始标签("Dog" 元素被消耗,但名称未指定)。在类内部读取元素,如 "Birthday",最后消耗结束标签。这忽略了当元素为空(例如,为了简单起见,像 Jerry 那样没有指定生日)时的正确处理。

此代码演示了如何在 ReadXml 中正确处理所有动物

public void ReadXml(System.Xml.XmlReader reader)
{
	reader.MoveToContent();
	Name = reader.GetAttribute("Name");
	Boolean isEmptyElement = reader.IsEmptyElement; // (1)
	reader.ReadStartElement();
	if (!isEmptyElement) // (1)
	{
		Birthday = DateTime.ParseExact(reader.
			ReadElementString("Birthday"), "yyyy-MM-dd", null);
		reader.ReadEndElement();
	}
}

实现中是否存在任何陷阱?

实际上不少

  1. 在使用 ToString() WriteXml 中 & 在 ReadXml 中读取时,请注意当前区域性。
  2. 不要在 WriteXml 中写包装元素,但在 ReadXml 中读取它!
  3. 在反序列化期间正确处理空元素。

陷阱一会在日期、浮点数值等情况下触发,这些值根据区域性显示不同。在英语国家,Rex 的生日可能显示为 10/22/2009。如果以这种格式保存文件并在具有不同区域设置的另一台计算机上打开,您将遇到麻烦。我总是喜欢为日期时间格式指定一个固定格式。 (我使用的简短 C# 格式说明备忘单 在此处。)

陷阱二会在您将基于属性的序列化与对某些类的 IXmlSerializable 实现混合使用时触发。

陷阱三会在元素为空或被省略时触发(多么令人惊讶!)。

为什么 ReadXml 和 WriteXml 的行为不对称?

实现选择是好的且有理由的,因为

  1. 调用代码必须预见元素名称是什么,以便创建正确类型的对象并在反序列化时填充。
  2. 您必须能够在 ReadXml 中处理属性,因此包装标签尚未被消耗。
  3. 您必须能够从外部定义包装标签的名称,以允许将同一类型序列化到不同的容器标签中。

第二点类似于说垃圾不需要知道它会进入哪个垃圾桶。它只需要知道如何描述自己,并且在你看到垃圾桶后才能自行分类。唯一令人费解的是,在 ReadXml 的情况下,垃圾自己打开了垃圾桶!但它不需要知道垃圾桶叫什么:ReadStartElement() 中没有使用参数指定名称。

如何处理空元素?

我必须说,我没有找到任何优雅的方式来处理空元素的反序列化。无论我尝试什么,我都必须进行额外的测试。我在 API 中找不到任何可以帮助我的方法。给微软的一个建议是向 ReadStartElement() 添加一个布尔返回值,如果元素为空则返回 false。如果您有一个空元素,可以在读取它之前检测到。如果有一个,则不要调用 ReadEndElement()

就我们的例子而言,这意味着 Dog 类应该移动到内容并读取 "Name" 属性。但是现在有了小小的区别。将 IsEmptyElement 的结果存储在一个布尔变量中。然后读取开始标签("Dog" 元素被消耗,但名称未指定)。仅当布尔值为 true 时,才在类内读取元素,如 "Birthday",并消耗结束标签。我真的意思是,如果布尔值为 true,则不要读取结束标签。您可能会错误地消耗下一个闭合标签,就像在 "Mouse" 的情况下一样,您也会消耗 "</Animals>"。

XML 序列化属性有哪些限制?

  • 不支持混合模式:所有文本属性都会合并成一个部分,并且在反序列化时会丢失位置信息。
  • 无法序列化接口:没有声明可以选择接口的具体实现。
  • 必须满足对象的要求(public 字段和属性,默认构造函数,... 请参阅 此链接)。
  • 许多 .NET 数据结构无法序列化(仅支持 ICollection IEnumerable 的实现,例如不支持 Dictionary)。
  • 无法实现动态行为,它是面向类型的,您无法根据动态约束更改序列化。例如,在某些情况下您不关心反序列化所有内容怎么办?那么 IXmlSerializable 会帮到您。

如何用 XML 属性实现相同的功能?

using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.ComponentModel;
using System.Text;
using System.Xml.Serialization;

namespace XmlWithAttributes
{
    public class Animal
    {
        public Animal() { }

        [XmlAttribute]
        public String Name { get; set; }

        [DefaultValue(typeof(DateTime), "0001-01-01T00:00:00")]
        public DateTime Birthday { get; set; }
    }
    public class Dog : Animal
    {
        public Dog() { }
    }
    public class Cat : Animal
    {
        public Cat() { }
    }
    public class Mouse : Animal
    {
        public Mouse() { }
    }
    public class Farm
    {
        public Farm() { Animals = new List<Animal>(); }

        [XmlElement("Dog", typeof(Dog))]
        [XmlElement("Cat", typeof(Cat))]
        [XmlElement("Mouse", typeof(Mouse))]
        public List<Animal> Animals { get; set; }
    }
}

生成的 XML 如下所示

<?xml version="1.0"?>
<Farm xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance 
	xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <Dog Name="Rex">
    <Birthday>2009-10-22T00:00:00</Birthday>
  </Dog>
  <Cat Name="Tom">
    <Birthday>1940-06-15T00:00:00</Birthday>
  </Cat>
  <Mouse Name="Jerry" />
</Farm>

不过,有一些限制。动物需要存储在 List 中,IList 不起作用,接口无法序列化。所有类型都必须是 public。日期格式无法通过属性声明进行修改。为了克服这些限制,使用 IXmlSerializable 的实现是一个简单的方法。

混合属性和 IXmlSerializable 实现如下所示

public class Animal : IXmlSerializable
{
	public Animal() { }
	public String Name { get; set; }
	public DateTime Birthday { get; set; }

	public System.Xml.Schema.XmlSchema GetSchema() { return null; }

	public void ReadXml(System.Xml.XmlReader reader)
	{
		reader.MoveToContent();
		Name = reader.GetAttribute("Name");
		Boolean isEmptyElement = reader.IsEmptyElement; // (1)
		reader.ReadStartElement();
		if (!isEmptyElement) // (1)
		{
			Birthday = DateTime.ParseExact(reader.
				ReadElementString("Birthday"), "yyyy-MM-dd", null);
			reader.ReadEndElement();
		}
	}

	public void WriteXml(System.Xml.XmlWriter writer)
	{
		writer.WriteAttributeString("Name", Name);
		if (Birthday != DateTime.MinValue)
			writer.WriteElementString("Birthday",
				Birthday.ToString("yyyy-MM-dd"));
	}
}
public class Dog : Animal
{
	public Dog() { }
}
public class Cat : Animal
{
	public Cat() { }
}
public class Mouse : Animal
{
	public Mouse() { }
}
public class Farm
{
	public Farm() { Animals = new List<Animal>(); }

	[XmlElement("Dog", typeof(Dog))]
	[XmlElement("Cat", typeof(Cat))]
	[XmlElement("Mouse", typeof(Mouse))]
	public List<Animal> Animals { get; set; }
}

这里的 ReadXml() 方法实现起来很棘手。如果您遵循了正确的指南,代码应该与上面写的相似。如果您忽略了对空元素的处理(注释为“(1)”的行),反序列化示例 XML 时会在解析 "Jerry" 时失败。WriteXml() 方法很简单且没问题,在这个例子中很难有其他写法。但在更简单的情况下,人们可能会倾向于在那里写入包含元素。在这里您可以看到为什么它通常不起作用。

实现克服了日期/时间问题,但仍然将列表作为具体类,并且 Farm 中的所有成员仍然必须是 public 。请注意,我们已经可以在 Animal 类中将设置器设置为 private NameBirthday)。

如何反序列化 XML 片段?

我必须解决这个问题才能读取所谓的流式 XML。我不确定这是否真的标准,但我必须为一些项目执行这样的任务,让我们用一种通用的方式来解释,一个源源不断地以 XML 形式流式传输对象,而没有一个围绕的主标签。这意味着实际文档将是无效的。有一种无需将其嵌入到人造标签中即可轻松处理片段的方法。我需要找出我曾经写过的代码,或者重新尝试写对它。

这篇文章 也提供了一些解决此问题的想法。

欢迎提问和评论,您的反馈对我非常宝贵。:-)

历史

  • 2009-10-24 添加了代码示例和更多细节
  • 2009-10-22 发布初始版本
© . All rights reserved.