BinaryFormatter.Deserialize如何创建新对象?

BinaryFormatter将流反序列化为对象时,它似乎在不调用构造函数的情况下创建新对象。

它是怎么做到的? 为什么? .NET中还有什么可以做到的吗?

这是一个演示:

 [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, SeekOrigin.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."); } } } 

输出:

Cars constructed: 1
But there are actually two.

调用构造函数有两件事(或者至少应该这样做)。

一种方法是为对象留出一定量的内存,并完成所有必要的内务处理,使其成为.NET世界其他部分的对象(请注意本说明中的一定数量的处理)。

另一种是将对象置于有效的初始状态,可能基于参数 – 这是构造函数中的实际代码将执行的操作。

反序列化通过调用FormatterServices.GetUninitializedObject与第一步完成相同的事情,然后通过将字段的值设置为等效于序列化期间记录的值(可能需要反序列化其他对象),与第二步完成相同的操作可以说是价值观)。

现在,反序列化将对象放入的状态可能与任何构造函数都不对应。 充其量它将是浪费(构造函数设置的所有值都将被覆盖),更糟糕的是它可能是危险的(构造函数有一些副作用)。 它也可能是不可能的(只有构造函数才能获取参数 – 序列化无法知道要使用的参数)。

你可以将它看作是一种特殊的构造函数,只用于反序列化(OO纯粹主义者会 – 并且应该 – 对于不构造的构造函数的想法感到不寒而栗,我的意思是这只是一个类比,如果你知道C ++会想到的话就记忆而言,重写new作品的方式和你有一个更好的类比,尽管仍然只是一个类比)。

现在,在某些情况下这可能是一个问题 – 也许我们只有一个只能由构造函数设置的readonly字段,或者我们可能有我们想要发生的副作用。

两者的解决方案是使用ISerializable覆盖序列化行为。 这将基于对ISerializable.GetObjectData调用进行SerializationInfo ,然后使用SerializationInfoStreamingContext字段调用特定的构造函数进行反序列化(所述构造函数甚至可以是私有的 – 这意味着大多数其他代码甚至都不会看到它)。 因此,如果我们可以反序列化readonly字段并且有任何我们想要的副作用(我们也可以做各种事情来控制序列化的内容和方式)。

如果我们只关心确保在构造时发生的反序列化会发生一些副作用,我们可以实现IDeserializationCallback并且在反序列化完成时调用IDeserializationCallback.OnDeserialization

至于其他与此相同的事情,在.NET中还有其他forms的序列化,但这就是我所知道的。 可以自己调用FormatterServices.GetUninitializedObject但是除非你强烈保证后续代码会将生成的对象置于有效状态(即,恰好是从串行化生成的数据反序列化对象时的情况)同样的对象)这样做是充满了并且是产生一个非常难以诊断的bug的好方法。

问题是,BinaryFormatter并没有真正制作你的特定对象。 它将对象图放回内存中。 对象图基本上是对象在内存中的表示; 这是在对象序列化时创建的。 然后,反序列化调用基本上只是将该图形作为一个打开指针的对象粘贴在内存中,然后它被转换为代码所实际存在的内容。 如果输入错误,则抛出exception。

至于你的具体例子,你只是在构建一辆汽车; 你只是制作了那辆车的精确复制品。 将序列化到流中时,会存储它的精确二进制副本。 反序列化时,不必构造任何东西。 它只是将图形作为对象粘贴在内存中的某个指针值上,并允许您随意执行任何操作。

你对car1!= car2的比较是正确的,因为不同的指针位置,因为Car是一个引用类型。

为什么? 坦率地说,只需去拉二进制表示就好了,而不必去拉动每个属性等等。

我不确定.NET中的其他任何东西是否使用相同的过程; 最有可能的候选者是在序列化期间以某种格式使用对象二进制文件的任何其他东西。

不知道为什么构造函数不会被调用,但我使用IDeserializationCallback作为解决方法。

还看一看

OnSerializingAttribute

OnSerializedAttribute

OnDeserializingAttribute

OnDeserializedAttribute