How/why XmlSerializer 在实现 IList<T> 时对 class 的处理方式不同吗?

How/why does XmlSerializer treat a class differently when it implements IList<T>?

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

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

我现在正在编写一些其他代码,这些代码使用此 class 中包含的数据,我认为如果我可以通过 IList<T> 接口访问数据会很有帮助,所以我修改了 class 以实现该接口。 (本例中的 "T" 是我自定义的另一个 classes。)这并不涉及向 class 添加任何新字段;我根据已经存储的数据实施了所有 required methods and properties

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

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

我在搜索这方面的信息时遇到问题,因为大多数涉及 XmlSerializerIList<T> 的问题似乎都涉及到人们试图 serialize/deserialize IList<T> 类型的变量].那不是我的情况;我在代码中的任何地方都没有 IList<T> 类型的变量。如果我不实现 IList<T> 接口,我的 class 序列化和反序列化就很好。

任何人都可以向我解释为什么 XmlSerializer 在我的 class 上调用 IList<T>.Add(),and/or 如何让它停止?

建议最好在 Unity3d (.NET 2.0) 中最终 运行 与此代码兼容。

没有 a good, minimal, complete code example 可靠地重现问题,将不可能提供任何具体的答案。

取而代之的是,这里有一些可能对您有所帮助的非特定注释:

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

XmlSerializer requires all collections to have an Add() method, as is spelled out in the documentation:

The XmlSerializer gives special treatment to classes that implement IEnumerable or ICollection. A class that implements IEnumerable must implement a public Add method that takes a single parameter. The Add method's parameter must be of the same type as is returned from the Current property on the value returned from GetEnumerator, or one of that type's bases. A class that implements ICollection (such as CollectionBase) in addition to IEnumerable must have a public Item indexed property (indexer in C#) that takes an integer, and it must have a public Count property of type integer. The parameter to the Add method must be the same type as is returned from the Item property, or one of that type's bases. For classes that implement ICollection, values to be serialized are retrieved from the indexed Item property, not by calling GetEnumerator.

此外,如果一个集合有自己的可设置属性,这些将不会被序列化docs:

中也有说明

The following items can be serialized using the XmLSerializer class:

  • Classes that implement ICollection or IEnumerable: Only collections are serialized, not public properties.

要了解这在实践中如何发挥作用,请考虑以下内容 class:

namespace V1
{
    // 
    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
{
    // 
    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() 方法。

示例 fiddle.

强制 XmlSerializer 无法将集合视为一个简单的对象,除了 implement IXmlSerializable and do it manually, which is quite burdensome. (There is a workaround with DataContractSerializer,即应用 [DataContract] 而不是 [CollectionDataContract] - - 然而 DataContractSerializer 直到 .Net 3.5 才被引入,所以它已经过时了。)

而不是实施 IList<T>,您可能只想引入一个扩展方法来遍历 class 中的值,如下所示:

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