【问题标题】:How/why does XmlSerializer treat a class differently when it implements IList<T>?XmlSerializer 在实现 IList<T> 时如何/为什么以不同的方式对待一个类?
【发布时间】:2015-07-22 02:12:01
【问题描述】:

XmlSerializer 在我的课堂上打电话给IList&lt;T&gt;.Add(),我不明白为什么。

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

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

我希望这不会以任何方式影响序列化。但是,当将 XML 数据反序列化到我的类中时,现在有一些东西正在调用我作为 IList&lt;T&gt; 接口的一部分实现的新 Add() 方法(这是一个问题,因为这个特定列表 IsReadOnly 和所以 Add() 抛出一个 @ 987654330@)。

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

我在搜索这方面的信息时遇到了麻烦,因为涉及XmlSerializerIList&lt;T&gt; 的大多数问题似乎都涉及人们试图序列化/反序列化IList&lt;T&gt; 类型的变量。那不是我的情况;我在代码中的任何地方都没有IList&lt;T&gt; 类型的变量。如果我不实现IList&lt;T&gt; 接口,我的类就可以进行序列化和反序列化。

谁能向我解释为什么XmlSerializer 在我的课堂上调用IList&lt;T&gt;.Add(),和/或如何让它停止?

理想情况下,建议应该与最终在 Unity3d (.NET 2.0) 中运行的代码兼容。

【问题讨论】:

  • 你不能只实现IReadOnlyList而不是IList吗?实现一个接口并在某些方法中抛出异常有点代码味道。
  • 我不熟悉IReadOnlyList,所以感谢您指出这一点。但是,此代码理想情况下应该在 Unity3d 中运行,我认为这将我限制在 .NET 2.0 中。

标签: c# xmlserializer


【解决方案1】:

XmlSerializer 要求所有集合都具有Add() 方法,如documentation 中所述:

XmlSerializer 对实现 IEnumerable 或 ICollection 的类进行特殊处理。实现 IEnumerable 的类必须实现采用单个参数的公共 Add 方法。 Add 方法的参数必须与从 GetEnumerator 返回的值的 Current 属性返回的类型相同,或该类型的基础之一。除了 IEnumerable 之外,实现 ICollection 的类(例如 CollectionBase)必须有一个公共 Item 索引属性(C# 中的索引器),它必须有一个公共 @ 987654335@ 整数类型的属性。 Add 方法的参数必须与从 Item 属性返回的类型相同,或者是该类型的基数之一。对于实现 ICollection 的类,要序列化的值是从索引的 Item 属性中检索的,而不是通过调用 GetEnumerator

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

可以使用 XmlSerializer 类对以下项目进行序列化:

  • 实现 ICollection 或 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&lt;double&gt; 的新版本。我添加接口并实现它,为调整列表大小的所有方法抛出异常:

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&lt;double&gt; 而不是IList&lt;double&gt;,则XmlSerializer 构造函数现在会由于缺少Add() 方法而引发异常。

例如fiddle

除了implement IXmlSerializabledo it manually 之外,没有办法强制XmlSerializer 将集合视为一个简单的对象,这相当繁琐。 (一个使用DataContractSerializer 的解决方法,即应用[DataContract] 而不是[CollectionDataContract]——但是DataContractSerializer 直到.Net 3.5 才被引入,所以就这样了。) p>

您可能不想实现IList&lt;T&gt;,而是简单地引入一个扩展方法来遍历类中的值,如下所示:

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

【讨论】:

    【解决方案2】:

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

    取而代之的是,以下是一些可能对您有所帮助的非特定说明:

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

    【讨论】:

    • 您是说 .NET 序列化假定在实现 IEnumerable 的任何对象上都存在 Add() 方法,即使 Add() 不是IEnumerable接口?
    • “假设” 是错误的词。 "Requires" 在序列化的上下文中更正确。显然,IEnumerable 接口本身不需要Add() 实现。但是序列化假设一个人会在某个时候反序列化。反序列化集合需要某种机制来填充该集合。 Microsoft 选择使要求简单且独立于实现的实际接口:该类型必须具有Add() 方法。有关详细信息,请参阅Collection Types in Data Contracts
    • 我认为您将XmlSerializerDataContractSerializer 混淆了。添加[DataContract]XmlSerializer 没有影响。
    • @dbc:你是对的,因此“在某些情况下”(即在使用 DataContractSerializer 和隐式使用它的场景时)。我的回答旨在广泛解决集合序列化的一般问题。 XmlSerializerDataContractSerializer 有很多相同的行为,所以我已经包含了与两者相关的点。
    猜你喜欢
    • 2011-04-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-07-23
    • 1970-01-01
    • 1970-01-01
    • 2014-12-02
    相关资源
    最近更新 更多