Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
195 views
in Technique[技术] by (71.8m points)

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

XmlSerializer is calling IList<T>.Add() on my class and I don't understand why.

I have a custom class (one of several classes in a hierarchy) containing data that I am converting to and from XML using XmlSerializer. In a previous version of my code, these classes did not implement any interfaces, and XML serialization and deserialization both seemed to work as expected.

I'm now working on some other code that uses the data contained in this class, and I thought it would be helpful if I could access the data through the IList<T> interface, so I modified my class to implement that interface. (The "T" in this case is another one of my custom classes.) This didn't involve adding any new fields to the class; I implemented all the required methods and properties in terms of data that was already being stored.

I was hoping that this would not affect the serialization in any way. However, when deserializing XML data into my class, something is now calling the new Add() method I implemented as part of the IList<T> interface (which is a problem because this particular list IsReadOnly and so Add() throws a NotSupportedException).

This happens even when the XML node for my class is simply <myClass/> with no XML attributes or children whatsoever; the XmlSerializer is apparently still creating a new myOtherClass (which is not named anywhere in the XML document) and trying to Add() it to the myClass.

I'm having trouble searching for information in this, because most questions involving XmlSerializer and IList<T> seem to involve people trying to serialize/deserialize a variable of type IList<T>. That is NOT my situation; I have no variables of type IList<T> anywhere in the code. My class serializes and deserializes just fine if I do NOT implement the IList<T> interface.

Can anyone explain to me why XmlSerializer is calling IList<T>.Add() on my class, and/or how to make it stop?

Suggestions should ideally be compatible with this code eventually running inside Unity3d (.NET 2.0).

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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.

Further, if a collection has its own settable properties, these will not be serialized. This is also spelled out in the docs:

The following items can be serialized using the XmLSerializer class:

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

To see how this plays out in practice, consider the following class:

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

If I serialize this to XML, I get:

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

Now say I want a new version of this that implements IList<double>. I add the interface and implement it, throwing exceptions for all methods that resize the list:

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

Now if I serialize the XML, I get:

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

As you can see, it now serializes as a collection of doubles, with the settable properties X and Y omitted. Then, when deserialized, the Add() method will get called instead of the set methods for X and Y, and throw an exception.

If I try to implement IReadOnlyList<double> instead of IList<double>, the XmlSerializer constructor now throws an exception because of the missing Add() method.

Example fiddle.

There is no way for force XmlSerializer to treat a collection as a straightforward object, other than to implement IXmlSerializable and do it manually, which is quite burdensome. (There is a workaround with DataContractSerializer, namely to apply [DataContract] instead of [CollectionDataContract] -- however DataContractSerializer was not introduced until .Net 3.5., so that's out.)

Instead of implementing IList<T>, you might want to simply introduce an extension method to iterate through the values in your class, like so:

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

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...