XML序列化和继承类型

86

在我 之前的问题 后,我一直在努力让我的对象模型序列化为 XML。但是现在我遇到了一个问题(惊不惊喜,意不意外!)。

我的问题是,我有一个集合,它是一个抽象基类类型,并且由具体派生类型填充。

我认为只需将 XML 属性添加到所有涉及的类中即可,一切都会很顺利。可惜,情况并非如此!

所以我在 Google 上进行了一些研究,现在我明白了 为什么它不起作用。因为XmlSerializer 实际上正在进行一些巧妙的反射以将对象序列化为/from XML,并且由于它基于抽象类型,因此它无法弄清楚它正在处理的内容。好的。

我找到了这个页面,看起来可能会很有帮助(尚未完全阅读/消化),但我也想将这个问题带到 StackOverflow 平台上,看看你是否有任何聪明的技巧/诀窍,以尽可能快/轻松地解决这个问题。

我还要补充的一件事是,我不想采用XmlInclude方法。与它相关的耦合太多了,而且该系统的这个领域正在进行大量开发,所以它将成为一个真正的维护头痛!


1
从您尝试序列化的类中提取一些相关的代码片段会很有帮助。 - Rex M
伙计:我重新打开了这个问题,因为我认为其他人可能会觉得这有用,但如果你不同意,可以随意关闭。 - JamesSugrue
有点困惑,因为这个帖子很久没有更新了? - Rob Cooper
这是答案:http://stackoverflow.com/questions/6737666/xml-serialization-problem-deserializing-an-abstract-property - Odys
7个回答

57

问题已解决!

好了,我终于做到了(尽管在这里得到了很多帮助!)。

简要概述:

目标:

  • 由于维护起来麻烦,我不想采用XmlInclude方法。
  • 一旦找到解决方案,我希望它可以快速地在其他应用程序中实现。
  • 可以使用抽象类型的集合,以及单个抽象属性。
  • 我不想在具体类中做“特殊”处理。

已确认的问题/注意事项:

  • XmlSerializer可以进行一些很酷的反射,但是对于抽象类型非常有限(即它只能与抽象类型本身的实例一起使用,而不能与子类一起使用)。
  • XML属性装饰器定义了XmlSerializer如何处理它发现的属性。也可以指定物理类型,但是这会在类和序列化程序之间创建紧密耦合(不好)。
  • 我们可以通过创建实现IXmlSerializable的类来实现自己的XmlSerializer。

解决方案

我创建了一个通用类,其中您将抽象类型指定为要使用的类型。这使得该类能够在抽象类型和具体类型之间“翻译”,因为我们可以硬编码转换(即我们可以获得比XmlSerializer更多的信息)。

然后我实现了IXmlSerializable接口,这很简单,但是在序列化时,我们需要确保将具体类的类型写入XML,以便在反序列化时进行转换。还需要注意它必须完全限定,因为两个类所在的程序集可能不同。当然,在此处还需要进行一些类型检查和其他处理。

由于XmlSerializer无法进行强制类型转换,因此我们需要提供代码来执行此操作,因此隐式操作符被重载(我甚至不知道你可以这样做!)。

AbstractXmlSerializer的代码如下:

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

namespace Utility.Xml
{
    public class AbstractXmlSerializer<AbstractType> : IXmlSerializable
    {
        // Override the Implicit Conversions Since the XmlSerializer
        // Casts to/from the required types implicitly.
        public static implicit operator AbstractType(AbstractXmlSerializer<AbstractType> o)
        {
            return o.Data;
        }

        public static implicit operator AbstractXmlSerializer<AbstractType>(AbstractType o)
        {
            return o == null ? null : new AbstractXmlSerializer<AbstractType>(o);
        }

        private AbstractType _data;
        /// <summary>
        /// [Concrete] Data to be stored/is stored as XML.
        /// </summary>
        public AbstractType Data
        {
            get { return _data; }
            set { _data = value; }
        }

        /// <summary>
        /// **DO NOT USE** This is only added to enable XML Serialization.
        /// </summary>
        /// <remarks>DO NOT USE THIS CONSTRUCTOR</remarks>
        public AbstractXmlSerializer()
        {
            // Default Ctor (Required for Xml Serialization - DO NOT USE)
        }

        /// <summary>
        /// Initialises the Serializer to work with the given data.
        /// </summary>
        /// <param name="data">Concrete Object of the AbstractType Specified.</param>
        public AbstractXmlSerializer(AbstractType data)
        {
            _data = data;
        }

        #region IXmlSerializable Members

        public System.Xml.Schema.XmlSchema GetSchema()
        {
            return null; // this is fine as schema is unknown.
        }

        public void ReadXml(System.Xml.XmlReader reader)
        {
            // Cast the Data back from the Abstract Type.
            string typeAttrib = reader.GetAttribute("type");

            // Ensure the Type was Specified
            if (typeAttrib == null)
                throw new ArgumentNullException("Unable to Read Xml Data for Abstract Type '" + typeof(AbstractType).Name +
                    "' because no 'type' attribute was specified in the XML.");

            Type type = Type.GetType(typeAttrib);

            // Check the Type is Found.
            if (type == null)
                throw new InvalidCastException("Unable to Read Xml Data for Abstract Type '" + typeof(AbstractType).Name +
                    "' because the type specified in the XML was not found.");

            // Check the Type is a Subclass of the AbstractType.
            if (!type.IsSubclassOf(typeof(AbstractType)))
                throw new InvalidCastException("Unable to Read Xml Data for Abstract Type '" + typeof(AbstractType).Name +
                    "' because the Type specified in the XML differs ('" + type.Name + "').");

            // Read the Data, Deserializing based on the (now known) concrete type.
            reader.ReadStartElement();
            this.Data = (AbstractType)new
                XmlSerializer(type).Deserialize(reader);
            reader.ReadEndElement();
        }

        public void WriteXml(System.Xml.XmlWriter writer)
        {
            // Write the Type Name to the XML Element as an Attrib and Serialize
            Type type = _data.GetType();

            // BugFix: Assembly must be FQN since Types can/are external to current.
            writer.WriteAttributeString("type", type.AssemblyQualifiedName);
            new XmlSerializer(type).Serialize(writer, _data);
        }

        #endregion
    }
}

那么,从这里开始,我们该如何告诉XmlSerializer使用我们的序列化程序而不是默认的呢?我们必须在Xml属性类型中传递我们的类型,例如:

[XmlRoot("ClassWithAbstractCollection")]
public class ClassWithAbstractCollection
{
    private List<AbstractType> _list;
    [XmlArray("ListItems")]
    [XmlArrayItem("ListItem", Type = typeof(AbstractXmlSerializer<AbstractType>))]
    public List<AbstractType> List
    {
        get { return _list; }
        set { _list = value; }
    }

    private AbstractType _prop;
    [XmlElement("MyProperty", Type=typeof(AbstractXmlSerializer<AbstractType>))]
    public AbstractType MyProperty
    {
        get { return _prop; }
        set { _prop = value; }
    }

    public ClassWithAbstractCollection()
    {
        _list = new List<AbstractType>();
    }
}

这里您可以看到,我们暴露了一个集合和一个单一属性,并且我们只需要添加名为type的参数到Xml声明中即可,非常简单!:D

注意:如果您使用此代码,我希望您能提及一下。这也将有助于吸引更多人加入社区 :)

现在,对于这里的答案,我不确定该怎么做,因为它们都有优点和缺点。我会赞同那些我认为有用的(无意冒犯那些没有用的),一旦我的声望足够高就会关闭这个问题 :)

有趣的问题,解决起来很有趣!:)


2
因为需要一个无参构造函数才能动态实例化该类。 - Silas Hansen
1
你好!我已经寻找这样的解决方案有一段时间了。我认为它很棒!虽然我无法弄清楚如何使用它,你介意给个例子吗? 你是在序列化你的类还是包含对象的列表? - Daniel
1
不错的代码。请注意,无参构造函数可以声明为privateprotected,以强制确保它不可用于其他类。 - tcovo
非常好的解决方案!我刚刚在WriteXml方法中用type.Assembly.GetName().Name(相同,但没有版本号)替换了type.AssemblyQualifiedName,以避免版本问题... - wexman
刚刚注意到如果集合为空会导致失败。在WriteXml方法中加入if(data!= null)可以解决这个问题。 - wexman
显示剩余11条评论

9

需要注意的一件事是,在XmlSerialiser构造函数中,可以传递一个类型数组,用于解决序列化器可能无法解析的类型。我曾经多次使用过这种方法,特别是在需要序列化集合或复杂数据结构的情况下,这些类型位于不同的程序集中等。

带有extraTypes参数的XmlSerialiser构造函数

编辑:我要补充的是,这种方法比XmlInclude属性等具有优势,因为你可以想出一种方法,在运行时发现和编译可能的具体类型列表,并将它们放入其中。


这就是我想要做的事情,但它并不像我想象的那么容易:https://dev59.com/vlHTa4cB1Zd3GeqPOAid - Luca
这是一篇非常老的帖子,但对于任何想要像我们一样实现此功能的人,请注意XmlSerializer的构造函数extraTypes参数不会缓存它动态生成的程序集。这给我们造成了几周的调试内存泄漏的代价。因此,如果您要使用接受答案代码的额外类型,请缓存序列化器。此行为在此处有所记录:https://support.microsoft.com/en-us/kb/886385 - Julien Lebot

3
一个可扩展的POCO框架永远无法可靠地序列化为XML。我这么说是因为我可以保证会有人来扩展你的类,并把它搞砸。
你应该考虑使用XAML来序列化你的对象图。它专门设计用于此,而XML序列化则不是。Xaml序列化器和反序列化器可以轻松处理泛型、基类和接口集合(只要集合本身实现了IList或IDictionary)。有一些注意事项,比如使用DesignerSerializationAttribute标记只读集合属性,但是重新编写代码以处理这些特殊情况并不难。

链接似乎已经失效。 - bkribbs
哦,好吧。我会删除那一部分。关于这个主题还有很多其他的资源。 - user1228

2

这确实是解决您问题的一种方案,但还有一个问题,这会削弱您使用“便携式”XML格式的意图。当您决定在程序的下一个版本中更改类并且需要支持序列化的两种格式 - 新格式和旧格式(因为您的客户仍在使用他们的旧文件/数据库,或者他们使用您产品的旧版本连接到您的服务器)时,会出现问题。但您不能再使用此序列化器,因为您已经使用了HTML标签。

type.AssemblyQualifiedName

看起来像

TopNamespace.SubNameSpace.ContainingClass+NestedClass, MyAssembly, Version=1.3.0.0, Culture=neutral, PublicKeyToken=b17a5c561934e089

这段文字是关于IT技术的,讲述了程序集属性和版本号的内容...

如果你试图更改程序集版本或者决定对其进行签名,那么这个反序列化就无法正常工作了...


2

这只是一个快速的更新,我没有忘记!

我正在做更多的研究,看起来我已经找到了解决方法,只需要整理代码。

目前为止,我有以下内容:

  • XmlSeralizer基本上是一个类,它对其序列化的类进行一些巧妙的反射。它根据Type确定要序列化的属性。
  • 问题发生的原因是发生了类型不匹配,它期望得到BaseType,但实际上收到了DerivedType。虽然您可能认为它会以多态方式处理它,但它不会,因为这将涉及到整个额外的反射和类型检查,而它不是为此设计的。

通过创建代理类作为序列化器之间的中介,可以覆盖此行为(代码待定)。这将基本确定派生类的类型,然后像往常一样对其进行序列化。然后,该代理类将将XML反馈到主序列化器。

敬请关注!^_^


1
更好的是,使用符号表示法:
[XmlRoot]
public class MyClass {
    public abstract class MyAbstract {} 
    public class MyInherited : MyAbstract {} 
    [XmlArray(), XmlArrayItem(typeof(MyInherited))] 
    public MyAbstract[] Items {get; set; } 
}

2
如果您了解您的类,那么这是最优雅的解决方案。但是,如果您从外部源加载新的继承类,则不幸的是无法使用它。 - Vladimir

1

我以前做过类似的事情。我通常会确保所有的XML序列化属性都在具体的类上,然后让该类的属性调用基类(在必要时)来获取将在序列化器调用这些属性时进行序列化/反序列化的信息。这样做需要更多的编码工作,但是比试图强制序列化器做正确的事情要好得多。


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