反序列化实现IXmlSerializable的类型集合永远运行

问题描述:

我有一个实现IXmlSerializable的类。这个类包含一些属性。序列化和反序列化类的单个实例工作正常。但是在集合类的情况下,序列化可以正常工作,但反序列化会永远运行。这是一段代码片段。我正在使用.Net 4.6.2。反序列化实现IXmlSerializable的类型集合永远运行

public class MyClass : IXmlSerializable 
{ 
    public int A { get; set; } 
    public int B { get; set; } 

    public XmlSchema GetSchema() 
    { 
     return null; 
    } 

    public void ReadXml(XmlReader reader) 
    { 
     this.A = Convert.ToInt32(reader.GetAttribute("A")); 
     this.B = Convert.ToInt32(reader.GetAttribute("B")); 
    } 

    public void WriteXml(XmlWriter writer) 
    { 
     writer.WriteAttributeString("A", this.A.ToString()); 
     writer.WriteAttributeString("B", this.B.ToString()); 
    } 
} 
class Program 
{ 
    static void Main(string[] args) 
    { 
     var instance = new MyClass { A = 1, B = 2 }; 
     Serialize(instance); 
     instance = Deserialize<MyClass>();//works fine 

     var list = new List<MyClass> { new MyClass { A = 10, B = 20 } }; 
     Serialize(list); 
     list = Deserialize<List<MyClass>>();//runs forever 
    } 

    private static void Serialize(object o) 
    { 
     XmlSerializer ser = new XmlSerializer(o.GetType()); 
     using (TextWriter writer = new StreamWriter("xml.xml", false, Encoding.UTF8)) 
     { 
      ser.Serialize(writer, o); 
     } 
    } 

    private static T Deserialize<T>() 
    { 
     XmlSerializer ser = new XmlSerializer(typeof(T)); 
     using (TextReader reader = new StreamReader("xml.xml")) 
     { 
      return (T)ser.Deserialize(reader); 
     } 
    } 
} 

您的问题是,作为documentation解释,ReadXml()必须消耗的包装元素以及它的内容:

ReadXml方法必须使用的时候写的信息重组的对象方法WriteXml

当调用此方法时,阅读器位于包装类型信息的开始标签上。也就是说,直接在指示序列化对象开始的开始标签上。 当此方法返回时,它必须从头到尾读取整个元素,包括其所有内容。与WriteXml方法不同,框架不会自动处理包装器元素。您的实施必须这样做。未能遵守这些定位规则可能会导致代码生成意外的运行时异常或损坏的数据。

MyClass.ReadXml()不这样做,这会导致无限循环时MyClass对象不是序列作为根元素。相反,你必须MyClass看起来是这样的:

public class MyClass : IXmlSerializable 
{ 
    public int A { get; set; } 
    public int B { get; set; } 

    public XmlSchema GetSchema() 
    { 
     return null; 
    } 

    public void ReadXml(XmlReader reader) 
    { 
     /* 
     * https://msdn.microsoft.com/en-us/library/system.xml.serialization.ixmlserializable.readxml.aspx 
     * 
     * When this method is called, the reader is positioned at the start of the element that wraps the information for your type. 
     * That is, just before the start tag that indicates the beginning of a serialized object. When this method returns, 
     * it must have read the entire element from beginning to end, including all of its contents. Unlike the WriteXml method, 
     * the framework does not handle the wrapper element automatically. Your implementation must do so. Failing to observe these 
     * positioning rules may cause code to generate unexpected runtime exceptions or corrupt data. 
     */ 
     var isEmptyElement = reader.IsEmptyElement; 
     this.A = XmlConvert.ToInt32(reader.GetAttribute("A")); 
     this.B = XmlConvert.ToInt32(reader.GetAttribute("B")); 
     reader.ReadStartElement(); 
     if (!isEmptyElement) 
     { 
      reader.ReadEndElement(); 
     } 
    } 

    public void WriteXml(XmlWriter writer) 
    { 
     writer.WriteAttributeString("A", XmlConvert.ToString(this.A)); 
     writer.WriteAttributeString("B", XmlConvert.ToString(this.B)); 
    } 
} 

现在你<MyClass>元素很简单,没有嵌套或可选元素。对于更复杂的自定义序列化,有几种策略可以用来保证您的ReadXml()方法完全按照它应该的方式读取,不多也不少。

首先,您可以拨打XNode.ReadFrom()将当前元素加载到XElement中。这需要更多的内存比直接从XmlReader解析,但更易于使用:

public class MyClass : IXmlSerializable 
{ 
    public int A { get; set; } 
    public int B { get; set; } 

    public XmlSchema GetSchema() 
    { 
     return null; 
    } 

    public void ReadXml(XmlReader reader) 
    { 
     var element = (XElement)XNode.ReadFrom(reader); 
     this.A = (int)element.Attribute("A"); 
     this.B = (int)element.Attribute("B"); 
    } 

    public void WriteXml(XmlWriter writer) 
    { 
     writer.WriteAttributeString("A", XmlConvert.ToString(this.A)); 
     writer.WriteAttributeString("B", XmlConvert.ToString(this.B)); 
    } 
} 

其次,你可以使用XmlReader.ReadSubtree()以确保所需的XML内容被消耗:

public class MyClass : IXmlSerializable 
{ 
    public int A { get; set; } 
    public int B { get; set; } 

    public XmlSchema GetSchema() 
    { 
     return null; 
    } 

    protected virtual void ReadXmlSubtree(XmlReader reader) 
    { 
     this.A = XmlConvert.ToInt32(reader.GetAttribute("A")); 
     this.B = XmlConvert.ToInt32(reader.GetAttribute("B")); 
    } 

    public void ReadXml(XmlReader reader) 
    { 
     // Consume all child nodes of the current element using ReadSubtree() 
     using (var subReader = reader.ReadSubtree()) 
     { 
      subReader.MoveToContent(); 
      ReadXmlSubtree(subReader); 
     } 
     reader.Read(); // Consume the end element itself. 
    } 

    public void WriteXml(XmlWriter writer) 
    { 
     writer.WriteAttributeString("A", XmlConvert.ToString(this.A)); 
     writer.WriteAttributeString("B", XmlConvert.ToString(this.B)); 
    } 
} 

最后几点注意事项:

  • 一定要同时处理<MyClass /><MyClass></MyClass>。这两种形式在语义上是相同的,发送系统也可以选择。

  • 推荐使用XmlConvert类中的方法将原语从XML转换为XML。这样做可以正确处理国际化。

  • 请务必使用或不使用缩进进行测试。有时候ReadXml()方法会消耗一个额外的XML节点,但是当启用缩进时该bug将被隐藏 - 因为它是被吃掉的空白节点。

  • 欲了解更多信息,请参阅How to Implement IXmlSerializable Correctly