How do I deserialize a collection of references to a struct equivalent?
NOTE: This question changed a little as I learned more about the problem, so please read it in its entirety. I've decided to leave it in its original form as it better describes how the problem was discovered and ultimately resolved.
Way back in the darkest depths of our project's history when we didn't really understand C# or the CLR as well as we might've, we created a type, let's call it MyType
. We created this type as a class
, a reference type.
However, it became apparent that MyType
should be a struct
, a value type so we made some changes to make it so and all was well until one day, we tried to deserialize some data that contained a collection of MyType
values. Well, not really, for when it was a reference type, that collection was a collection of references. Now when it deserializes, the collection deserializes fine, using the default constructor of MyType
, then later when the actual references deserialize, they are orphaned, leaving us with a collection of empty values.
So, we thought, "let's map the type to a reference type, MyTypeRef
on load so that the references properly resolve, then convert back to our real type for use during execution time and reserialization". So we did (using our own binder), but alas, it didn't work because now we get an error that tells us MyTypeRef[]
can't be converted to MyType[]
even if we have an implicit conversion between MyTypeRef
and MyType
.
So, we're stuck. How do we get a collection serialized as a collection of reference type MyType
to deserialize as a collection of value type MyType
?
Update
Some investigation (see comments and code below) has shown that it is the immutable nature of the newMyType
and its used of ISerializable
for serialization that has caused the real issue. I still don't see why that should be the case, but if I use private
set accessors instead of ISerializable
, the new MyType
will load the old (note that the ISerializable
interface is called if I use it but the collection only even contains default values).
Some code
// Use a List<T> in a class that also implements ISerializable and
// save an instance of that class with a BinaryFormatter (code omitted)
// Save with this one.
[Serializable]
pu开发者_如何学运维blic class MyType
{
private string test;
public string Test
{
get { return this.test; }
set { this.test = value; }
}
public MyType()
{
}
}
// Load with this one.
[Serializable]
public class MyType : ISerializable
{
private string test;
public string Test
{
get { return this.test; }
set { this.test = value; }
}
public MyType()
{
}
public MyType(SerializationInfo info, StreamingContext context)
{
info.AddValue("test", this.test);
}
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
this.test = info.GetString("test");
}
}
Note that when loading, the elements are all nulls. Remove ISerializable
from the second definition and load and all works. Change class
to struct
on the load code and the same behaviour is visible. It is as though the collection will only deserialize successfully using the set accessors.
Update Two
So, I found the problem (see my answer below) but I doubt anyone would've known the answer from reading my question. I missed out an important detail that I didn't even realise was important at the time. My sincerest apologies to those who tried to help.The collection that is loaded containing MyType
is immediately copied to another collection during GetObjectData
. The answer below explains why this turns out to be important. Here is some additional sample code to that given above that should provide a complete example:
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
this.myTypeCollection = new List<MyType>();
var loadedCollection = (List<MyType>)info.GetValue(
"myTypeCollection",
typeof(List<MyType>));
this.myTypeCollection.AddRange(loadedCollection);
}
Not sure to understand, but if I do this on save:
[Serializable]
public class MyTypeColl
{
public MyTypeColl()
{
}
public List<MyType> Coll { get; set; }
}
[Serializable]
public class MyType
{
private string test;
public string Test
{
get { return this.test; }
set { this.test = value; }
}
public MyType()
{
}
}
// save code
MyTypeColl coll = new MyTypeColl();
coll.Coll = new List<MyType>();
coll.Coll.Add(new MyType{Test = "MyTest"});
BinaryFormatter bf = new BinaryFormatter();
using (FileStream stream = new FileStream("test.bin", FileMode.OpenOrCreate))
{
bf.Serialize(stream, coll);
}
And this on load:
[Serializable]
public struct MyType
{
private string test;
public string Test
{
get { return this.test; }
set { this.test = value; }
}
}
// load code
BinaryFormatter bf = new BinaryFormatter();
using (FileStream stream = new FileStream("test.bin", FileMode.Open))
{
MyTypeColl coll = (MyTypeColl)bf.Deserialize(stream);
Console.WriteLine(coll.Coll[0].Test);
}
It displays "MyTest" successfuly. So what am I missing?
I feel somewhat foolish now but I discovered the issue. I have updated the question with additional sample code.
Here is the basics of what the code was doing in the GetObjectData
method:
- Load the collection
- Copy the collection into another collection
Here is the sequence that occurred at execution:
- Collection loaded but elements are not
- Copied null/default elements to another collection and discarded loaded collection variable
- Elements of loaded collection are deserialized
Note that it is only after point 3 that the entire collection is deserialized but I have already done my copy, hence having nothing.
The fix was to use the OnDeserialized
attribute to specify a method to call once the entire object was deserialized. Then, in GetObjectData
I save away a reference to the collection that is loaded, but copy its contents in the OnDeserialized
method once it is fully deserialized.
Side note
In fact, to keep the deserialization code all in GetObjectData
, I saved away a delegate to perform the copy and I call that delegate later - that way it is clear in GetObjectData
exactly what will happen to finish deserialization.
Code
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
var loadedCollection = (List<MyType>)info.GetValue(
"myTypeCollection",
typeof(List<MyType>));
this.myTypeCollectionLoader = () =>
{
this.myTypeCollection.AddRange(loadedCollection);
};
}
[OnDeserialized]
private void OnDeserialized(StreamingContext context)
{
this.myTypeCollectionLoader();
this.myTypeCollectionLoader = null;
}
精彩评论