.NET XML序列化的陷阱?

121

在进行C# XML序列化时,我遇到了一些棘手的问题,现在我想分享一下:


using System;
using System.Collections.Generic;
using System.Text;
using System.Xml.Serialization;

[XmlRoot("dictionary")]
public class SerializableDictionary<TKey, TValue> : Dictionary<TKey, TValue>, IXmlSerializable
{      
    public System.Xml.Schema.XmlSchema GetSchema()
    {
        return null;
    }

    public void ReadXml(System.Xml.XmlReader reader)
    {
        XmlSerializer keySerializer = new XmlSerializer(typeof(TKey));
        XmlSerializer valueSerializer = new XmlSerializer(typeof(TValue));

        bool wasEmpty = reader.IsEmptyElement;
        reader.Read();

        if (wasEmpty)
            return;

        while (reader.NodeType != System.Xml.XmlNodeType.EndElement)
        {
            reader.ReadStartElement("item");

            reader.ReadStartElement("key");
            TKey key = (TKey)keySerializer.Deserialize(reader);
            reader.ReadEndElement();

            reader.ReadStartElement("value");
            TValue value = (TValue)valueSerializer.Deserialize(reader);
            reader.ReadEndElement();

            this.Add(key, value);

            reader.ReadEndElement();
            reader.MoveToContent();
        }
        reader.ReadEndElement();
    }

    public void WriteXml(System.Xml.XmlWriter writer)
    {
        XmlSerializer keySerializer = new XmlSerializer(typeof(TKey));
        XmlSerializer valueSerializer = new XmlSerializer(typeof(TValue));

        foreach (TKey key in this.Keys)
        {
            writer.WriteStartElement("item");

            writer.WriteStartElement("key");
            keySerializer.Serialize(writer, key);
            writer.WriteEndElement();

            writer.WriteStartElement("value");
            TValue value = this[key];
            valueSerializer.Serialize(writer, value);
            writer.WriteEndElement();

            writer.WriteEndElement();
        }
    }
}

还有其他需要注意的XML序列化问题吗?


寻找更多的陷阱哈哈,也许你可以帮助我: https://dev59.com/JE3Sa4cB1Zd3GeqPtkLB - Shimmy Weitzhandler
1
此外,您还需要查看Charles Feduke的可序列化字典实现,他使xml编写器注意到可归属成员和常规成员之间的区别,以便由默认序列化程序进行序列化: http://www.deploymentzone.com/2008/09/19/idictionarytkeytvalue-ixmlserializable-and-lambdas/ - Shimmy Weitzhandler
这似乎没有完全捕获所有的陷阱。我在构造函数中设置了IEqualityComparer,但在此代码中并未序列化。有什么想法可以扩展这个Dictionary以包括这一部分信息吗?那个信息可以通过Type对象处理吗? - ColinCren
19个回答

27

另一个巨大的陷阱:当通过网页(ASP.NET)输出XML时,不要包含Unicode字节顺序标记。当然,使用或不使用BOM的方法几乎相同:

错误(包括BOM):

XmlTextWriter wr = new XmlTextWriter(stream, new System.Text.Encoding.UTF8);

好的:

XmlTextWriter  wr = new XmlTextWriter(stream, new System.Text.UTF8Encoding(false))

你可以明确地传递false来指示你不想要BOM。注意Encoding.UTF8UTF8Encoding之间的明显区别。
开头的三个额外的BOM字节是(0xEFBBBF)或(239 187 191)。
参考:http://chrislaco.com/blog/troubleshooting-common-problems-with-the-xmlserializer/

4
如果您不仅告诉我们“是什么”,还告诉我们“为什么”,那么您的评论将更有用。 - Neil
1
这与XML序列化没有真正关系...只是一个XmlTextWriter问题。 - Thomas Levesque
7
不相关于问题,并且在.NET 2.0或以上版本中不应该使用XmlTextWriter。 - John Saunders
非常有用的参考链接。谢谢。 - Anil Vangari

22

我暂时无法发表评论,因此我将在Dr8k的帖子下进行评论并提出另一个观察点。私有变量被公开作为公共getter/setter属性,并通过这些属性进行序列化/反序列化。我们以前的工作经常这样做。

需要注意的是,如果您在这些属性中有任何逻辑,则逻辑会运行,因此有时序列化的顺序实际上很重要。成员变量按照它们在代码中的顺序隐式排序,但没有保证,特别是当您继承另一个对象时。显式排序非常麻烦。

我过去曾经因此而受到伤害。


17
在搜索如何显式设置字段顺序时,我发现了这篇文章。这可以通过属性来实现: [XmlElementAttribute(Order = 1)] public int Field {...} 缺点是必须为类中的所有字段及其后代指定该属性! 我认为您应该在您的帖子中加入这个信息。 - Cristian Diaconescu

15

3
直接从文档中获取的信息:注意,缓冲区包含已分配但未使用的字节。例如,如果将字符串“test”写入MemoryStream对象,则从GetBuffer返回的缓冲区长度为256,而不是4,其中252个字节未使用。要仅获取缓冲区中的数据,请使用ToArray方法;但是,ToArray会在内存中创建数据的副本。 - realgt
刚刚看到这个。现在听起来不再像胡言乱语了。 - John Saunders
以前从来没听说过,这对调试非常有帮助。 - Ricky

10

如果序列化器遇到一个以接口作为其类型的成员/属性,它就不会进行序列化。例如,以下内容不会序列化为XML:

public class ValuePair
{
    public ICompareable Value1 { get; set; }
    public ICompareable Value2 { get; set; }
}

尽管这会进行序列化:

public class ValuePair
{
    public object Value1 { get; set; }
    public object Value2 { get; set; }
}

如果您收到一条消息为“未解析成员类型…”的异常,那么可能是出现了这种情况。 - Kyle Krull

9
通过 yield 返回生成的 IEnumerables<T> 是不可序列化的。这是因为编译器会生成一个单独的类来实现 yield,而该类没有标记为可序列化。

这适用于“其他”序列化,即[Serializable]属性。但这对XmlSerializer也不起作用。 - Tim Robinson
1
请注意,当一个类实现ICollection接口时,只有该类所包含的集合会被序列化。任何添加到该类的公共属性或字段都不会被序列化。这是一个令人震惊的时刻,也浪费了一个美好的下午。 - Hugh W
也许你的意思是生成了一些输出,但可以认为它是不完整的。我发现实现IEnumerable<>接口的类可以被XmlSerializer序列化,但在我看来是不完整的。问题在于枚举的项成为唯一序列化的内容。其他公共数据成员将被忽略。枚举成为该类的全部。 - H2ONaCl

8

您无法序列化只读属性。即使您从不打算使用反序列化将XML转换为对象,也必须具有getter和setter。

出于同样的原因,您无法序列化返回接口的属性:反序列化程序无法知道要实例化哪个具体类。


1
实际上,即使集合属性没有setter,您也可以对其进行序列化,但必须在构造函数中初始化它,以便反序列化可以向其中添加项。 - Thomas Levesque

7

哦,这是一个好问题:由于XML序列化代码是生成并放置在一个单独的DLL中,当您的代码出现错误导致序列化器崩溃时,您将无法获得任何有意义的错误信息。只会出现类似"无法定位s3d3fsdf.dll"之类的信息。很不错。


11
你可以使用XML“序列化器生成工具(Sgen.exe)”预先生成DLL并将其与应用程序一起部署。 - huseyint

6

无法序列化没有无参构造函数的对象(我也曾被这个问题困扰)。

并且由于某些原因,以下属性中,Value被序列化了,但FullName没有:

    public string FullName { get; set; }
    public double Value { get; set; }

我从来没有想过为什么,我只是将Value更改为internal...


4
无参构造函数可以是私有或受保护的。这对于 XML 序列化器来说已经足够了。关于 FullName 的问题真的很奇怪,不应该发生... - Massimiliano
@Yacoder:可能是因为类型不是double?,而是double吧? - abatishchev
FullName 可能为 null,因此在序列化时不会生成任何 XML。 - Jesper

5

4
如果您尝试序列化包含T的子类实例的数组、List<T>IEnumerable<T>,则需要使用XmlArrayItemAttribute列出所有正在使用的子类型,否则在序列化时运行时会得到一个无用的System.InvalidOperationException异常。
以下是来自文档的完整示例的一部分。
public class Group
{  
   /* The XmlArrayItemAttribute allows the XmlSerializer to insert both the base 
      type (Employee) and derived type (Manager) into serialized arrays. */

   [XmlArrayItem(typeof(Manager)), XmlArrayItem(typeof(Employee))]
   public Employee[] Employees;

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接