1

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

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);
        }
    }
}
4

1 回答 1

2

您的问题是,如文档中所述,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. 这比直接从 an 解析需要更多的内存,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 节点,但是当启用缩进时错误将被隐藏——因为它是被吃掉的空白节点。

  • 如需进一步阅读,请参阅如何正确实现 IXmlSerializable

于 2017-03-08T17:23:51.647 回答