当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.
答案 0 :(得分:19)
调用构造函数有两件事(或者至少应该这样做)。
一种方法是为对象留出一定量的内存,并完成所有必要的内务处理,使其成为.NET世界其他部分的对象(请注意本说明中的一定数量的处理)。
另一种方法是将对象置于有效的初始状态,可能是基于参数 - 这就是构造函数中的实际代码所做的。
反序列化通过调用FormatterServices.GetUninitializedObject
与第一步完成相同的事情,然后通过将字段的值设置为等效于序列化期间记录的值(与可能需要将其他对象反序列化为所述值。)
现在,反序列化将对象放入的状态可能与任何构造函数都不对应。充其量它将是浪费(构造函数设置的所有值都将被覆盖),更糟糕的是它可能是危险的(构造函数有一些副作用)。它也可能是不可能的(只有构造函数才能获取参数 - 序列化无法知道要使用的参数)。
你可以将它视为一种特殊的构造函数,仅用于反序列化(OO纯粹主义者会 - 并且应该 - 对于不构造的构造函数的想法感到不寒而栗,我的意思是这只是一个类比,如果你知道的话C ++想到了覆盖new
的方式,就内存而言,你有一个更好的类比,尽管只是一个类比)。
现在,在某些情况下这可能是一个问题 - 也许我们有readonly
个字段只能由构造函数设置,或者我们可能有我们想要的副作用发生。
两者的解决方案是使用ISerializable
覆盖序列化行为。这将基于对ISerializable.GetObjectData
的调用进行序列化,然后调用具有SerializationInfo
和StreamingContext
字段的特定构造函数进行反序列化(所述构造函数甚至可以是私有的 - 这意味着大多数其他代码甚至不会看见)。因此,如果我们可以反序列化readonly
字段并且有任何我们想要的副作用(我们也可以做各种事情来控制序列化的内容和方法)。
如果我们只关心确保在反序列化时会发生一些副作用,那么我们就可以实现IDeserializationCallback
,并且在反序列化完成后我们会调用IDeserializationCallback.OnDeserialization
。
至于其他与此相同的事情,在.NET中还有其他形式的序列化,但这就是我所知道的。可以自己调用FormatterServices.GetUninitializedObject
,但是除非您强烈保证后续代码会将生成的对象置于有效状态(即,正好是在生成的数据中反序列化对象时所处的情况通过序列化相同类型的对象)这样做是非常充实的,也是产生一个非常难以诊断的错误的好方法。
答案 1 :(得分:3)
问题是,BinaryFormatter并没有真正制作你的特定对象。它将对象图放回内存中。对象图基本上是对象在内存中的表示;这是在序列化对象时创建的。然后,反序列化调用基本上只是将该图形作为开放指针处的对象粘贴在内存中,然后将其转换为代码实际存在的内容。如果输入错误,则抛出异常。
至于你的具体例子,你只是在构建一辆汽车;你只是制作了那辆车的精确复制品。将序列化到流中时,会存储它的精确二进制副本。反序列化时,不必构造任何东西。它只是将图形作为对象粘贴在内存中的某个指针值上,并允许您随意使用它。
你对car1!= car2的比较是正确的,因为不同的指针位置,因为Car是一个参考类型。
为什么呢?坦率地说,只需要拉动二进制表示,而不是必须去拉动每个属性以及所有这些。
我不确定.NET中的其他内容是否使用相同的过程;最有可能的候选者是在序列化期间以某种格式使用对象二进制文件的任何其他东西。
答案 2 :(得分:1)
不确定为什么没有调用构造函数,但我使用IDeserializationCallback
作为解决方法。
还要看看