XmlSerializer在实现IList时如何/为什么以不同的方式对待一个类

本文关键字:方式 一个 IList 实现 为什么 XmlSerializer | 更新日期: 2023-09-27 18:03:50

XmlSerializer在我的班上调用IList<T>.Add(),我不明白为什么。

我有一个自定义类(层次结构中的几个类之一),其中包含我使用XmlSerializer转换为XML的数据。在我以前的代码版本中,这些类没有实现任何接口,XML序列化和反序列化似乎都能按预期工作。

我现在正在处理使用该类中包含的数据的其他代码,并且我认为如果我可以通过IList<T>接口访问数据将会有所帮助,因此我修改了我的类来实现该接口。(这里的"T"是我的另一个自定义类。)这并不涉及向类中添加任何新字段;我根据已经存储的数据实现了所有必需的方法和属性。

我希望这不会以任何方式影响序列化。然而,当将XML数据反序列化到我的类中时,有些东西现在正在调用我作为IList<T>接口的一部分实现的新的Add()方法(这是一个问题,因为这个特定的列表IsReadOnlyAdd()抛出一个NotSupportedException)。

即使我的类的XML节点只是没有XML属性或任何子节点的<myClass/>,也会发生这种情况;XmlSerializer显然仍在创建一个新的myOtherClass(在XML文档中的任何地方都没有命名),并试图将其Add()myClass

我在搜索信息时遇到了麻烦,因为大多数涉及XmlSerializerIList<T>的问题似乎都涉及到试图序列化/反序列化IList<T>类型变量的人。那不是我的处境;我在代码的任何地方都没有IList<T>类型的变量。如果我不实现IList<T>接口,我的类可以很好地序列化和反序列化。

谁能向我解释为什么XmlSerializer在我的类上调用IList<T>.Add(),和/或如何使其停止?

建议应该理想地与最终运行在Unity3d(。NET 2.0)。

XmlSerializer在实现IList<T>时如何/为什么以不同的方式对待一个类

XmlSerializer 要求所有集合都有一个Add()方法,如文档中所述:

XmlSerializer对实现IEnumerable或ICollection的类给予特殊处理。实现IEnumerable的类必须实现一个接受单个形参的public Add 方法。 Add 方法的形参必须与 GetEnumerator 返回值的 Current 属性返回的类型相同,或者是该类型的基类型之一。除了IEnumerable之外,实现ICollection(比如CollectionBase)的类必须有一个public Item 索引属性(c#中的indexer),它必须有一个public Count integer类型的属性。 Add 方法的参数必须与 Item 属性返回的类型相同,或者是该类型的基类型之一。对于实现iccollection的类,要序列化的值是从索引 Item 属性中检索的,而不是通过调用 GetEnumerator

此外,如果集合有自己的可设置属性,这些属性将不会被序列化。这在文档中也有详细说明:

可以使用XmLSerializer类序列化以下项:

  • 实现icollections或IEnumerable的类:只有集合被序列化,而不是公共属性。

要了解这在实践中是如何发挥作用的,请考虑以下类:

namespace V1
{
    // https://stackoverflow.com/questions/31552724/how-why-does-xmlserializer-treat-a-class-differently-when-it-implements-ilistt
    public class Vector2
    {
        public double X { get; set; }
        public double Y { get; set; }
        public Vector2() { }
        public Vector2(double x, double y)
            : this()
        {
            this.X = x;
            this.Y = y;
        }
        public double this[int coord]
        {
            get
            {
                switch (coord)
                {
                    case 0:
                        return X;
                    case 1:
                        return Y;
                    default:
                        throw new ArgumentOutOfRangeException();
                }
            }
            set
            {
                switch (coord)
                {
                    case 0:
                        X = value;
                        break;
                    case 1:
                        Y = value;
                        break;
                    default:
                        throw new ArgumentOutOfRangeException();
                }
            }
        }
    }
}

如果我将它序列化为XML,我得到:

<Vector2>
    <X>1</X>
    <Y>2</Y>
</Vector2>

现在假设我想要一个实现IList<double>的新版本。我添加接口并实现它,为所有调整列表大小的方法抛出异常:

namespace V2
{
    // https://stackoverflow.com/questions/31552724/how-why-does-xmlserializer-treat-a-class-differently-when-it-implements-ilistt
    public class Vector2 : V1.Vector2, IList<double>
    {
        public Vector2() : base() { }
        public Vector2(double x, double y) : base(x, y) { }
        #region IList<double> Members
        public int IndexOf(double item)
        {
            for (var i = 0; i < Count; i++)
                if (this[i] == item)
                    return i;
            return -1;
        }
        public void Insert(int index, double item)
        {
            throw new NotImplementedException();
        }
        public void RemoveAt(int index)
        {
            throw new NotImplementedException();
        }
        #endregion
        #region ICollection<double> Members
        public void Add(double item)
        {
            throw new NotImplementedException();
        }
        public void Clear()
        {
            throw new NotImplementedException();
        }
        public bool Contains(double item)
        {
            return IndexOf(item) >= 0;
        }
        public void CopyTo(double[] array, int arrayIndex)
        {
            foreach (var item in this)
                array[arrayIndex++] = item;
        }
        public int Count
        {
            get { return 2; }
        }
        public bool IsReadOnly
        {
            get { return true; }
        }
        public bool Remove(double item)
        {
            throw new NotImplementedException();
        }
        #endregion
        #region IEnumerable<double> Members
        public IEnumerator<double> GetEnumerator()
        {
            yield return X;
            yield return Y;
        }
        #endregion
        #region IEnumerable Members
        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
        #endregion
    }
}

现在如果我序列化XML,我得到:

<ArrayOfDouble>
    <double>1</double>
    <double>2</double>
</ArrayOfDouble>

可以看到,它现在序列化为双精度的集合,可设置的属性XY省略了。然后,在反序列化时,将调用Add()方法而不是XY的set方法,并抛出异常。

如果我尝试实现IReadOnlyList<double>而不是IList<double>, XmlSerializer构造函数现在会抛出异常,因为缺少Add()方法。

小提琴例子。

没有办法强制XmlSerializer将集合视为一个直接的对象,除了实现IXmlSerializable并手动执行之外,这是相当繁重的。(DataContractSerializer的一个解决方案,即应用[DataContract]而不是[CollectionDataContract]——然而DataContractSerializer直到。net 3.5才被引入。)

与实现IList<T>不同,您可能希望简单地引入一个扩展方法来迭代类中的值,如下所示:
    public static class Vector2Extensions
    {
        public static IEnumerable<double> Values(this Vector2 vec)
        {
            if (vec == null)
                throw new ArgumentNullException();
            yield return vec.X;
            yield return vec.Y;
        }
    }

如果没有一个好的,最小的完整的代码示例来可靠地再现问题,将不可能提供任何特定的答案。

作为替代,这里有一些非特定的注意事项可能会对您有所帮助:

  1. 。. NET序列化将集合类型与其他类型区别对待。默认情况下,实现任何IEnumerable接口(例如IList<T>)的类型被认为是一个集合。这种类型通过枚举集合和存储单个元素来序列化。在反序列化方面,. net假定它可以使用Add()方法来填充反序列化的对象。任何从Add()方法抛出异常,或者更糟,根本不实现异常的类型都有祸了。
  2. 在某些情况下,用[DataContract]属性标记类型可能是合适的。这将覆盖默认行为,允许您的类型被视为非集合类型。
  3. 在其他情况下,你真的不应该首先实现IList<T>,而是应该以不同的方式公开元素的枚举,例如作为返回枚举的属性。缺乏一个好的代码示例(好吧,任何代码示例),不可能说这在你的场景中是否正确,但我想说至少有50/50的机会是正确的。