How does BinaryFormatter.Deserialize create new objects?
When BinaryFormatter
deserializes a stream into objects, it appears to create new objects without calling constructors.
How is it doing this? And why? Is there anything else in .NET that does this?
Here's a demo:
[Serializable]
public class Car
{
public static int constructionCount = 0;
public Car()
{
constructionCount++;
}
}
public class Test
{
public static void Main(string[] args)
{
// Construct a car
Car car1 = new Car();
// Serialize and then deserialize to create a second, identical car
MemoryStream stream = new MemoryStream();
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(stream, car1);
stream.Seek(0, SeekOrigi开发者_Python百科n.Begin);
Car car2 = (Car)formatter.Deserialize(stream);
// Wait, what happened?
Console.WriteLine("Cars constructed: " + Car.constructionCount);
if (car2 != null && car2 != car1)
{
Console.WriteLine("But there are actually two.");
}
}
}
Output:
Cars constructed: 1
But there are actually two.
There are two things calling a constructor does (or at least should do).
One is to set aside a certain amount of memory for the object and does all the housekeeping necessary for it to be an object to the rest of the .NET world (note certain amount of handwaving in this explanation).
The other is to put the object into a valid initial state, perhaps based on parameters - this is what the actual code in the constructor will do.
Deserialisation does much the same thing as the first step by calling FormatterServices.GetUninitializedObject
, and then does much the same thing as the second step by setting the values for fields to be equivalent to those that were recorded during serialisation (which may require deserialising other objects to be said values).
Now, the state that deserialisation is putting the object into may not correspond to that possible by any constructor. At best it will be wasteful (all values set by the constructor will be overwritten) and at worse it could be dangerous (constructor has some side-effect). It could also just be impossible (only constructor is one that takes parameters - serialisation has no way of knowing what arguments to use).
You could look at it as a special sort of constructor only used by deserialisation (OO purists will - and should - shudder at the idea of a constructor that doesn't construct, I mean this as an analogy only, if you know C++ think of the way overriding new
works as far as memory goes and you've an even better analogy, though still just an analogy).
Now, this can be a problem in some cases - maybe we have readonly
fields that can only be set by a constructor, or maybe we have side-effects that we want to happen.
A solution to both is to override serialisation behaviour with ISerializable
. This will serialise based on a call to ISerializable.GetObjectData
and then call a particular constructor with SerializationInfo
and StreamingContext
fields to deserialise (said constructor can even be private - meaning most other code won't even see it). Hence if we can deserialise readonly
fields and have any side-effects we want (we can also do all manner of things to control just what is serialised and how).
If we just care about ensuring some side-effect happens on deserialisation that would happen on construction, we can implement IDeserializationCallback
and we will have IDeserializationCallback.OnDeserialization
called when deserialisation is complete.
As for other things that do the same thing as this, there are other forms of serialisation in .NET but that's all I know of. It is possible to call FormatterServices.GetUninitializedObject
yourself but barring a case where you have a strong guarantee that subsequent code will put the object produced into a valid state (i.e. precisely the sort of situation you are in when deserialising an object from data produced by serialising the same sort of object) doing such is fraught and a good way to produce a really hard to diagnose bug.
The thing is, BinaryFormatter isn't really making your particular object. It's putting an object graph back into memory. The object graph is basically the representation of your object in memory; this was created when the object is serialized. Then, the deserialize call basically just sticks that graph back in memory as an object at an open pointer, and then it gets casted to what it actually is by the code. If it's casted wrong, then an exception is thrown.
As to your particular example, you are only really constructing one car; you are just making an exact duplicate of that car. When you serialize it off into the stream, you store an exact binary copy of it. When you deserialize it, you don't have to construct anything. It just sticks the graph in memory at some pointer value as an object and lets you do whatever you want with it.
Your comparison of car1 != car2 is true because of that different pointer location, since Car is a reference type.
Why? Frankly, it's easy to just go pull the binary representation, rather than having to go and pull each property and all that.
I'm not sure whether anything else in .NET uses this same procedure; the most likely candidates would be anything else that uses an object's binary in some format during serialization.
Not sure why the constructor does not get called but I use IDeserializationCallback
as a work around.
also take a look at
OnSerializingAttribute
OnSerializedAttribute
OnDeserializingAttribute
OnDeserializedAttribute
精彩评论