2010-08-17 49 views
18

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

这是怎么回事?为什么? .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

好问题。要解决这个问题,您需要在反序列化过程中做一些指针/参考修正,这可能很难甚至不可能。请注意,“新车”只被调用一次。你可能想在2个过程中尝试这个。 – leppie 2010-08-17 08:03:52

+0

可能重复的[DataContractSerializer不会调用我的构造函数??](http://stackoverflow.com/questions/1076730/datacontractserializer-doesnt-call-my-constructor) – 2010-08-17 08:05:14

+2

注意:我链接到的另一个问题是关于DataContractSerializer ,但BinaryFormatter – 2010-08-17 08:06:53

回答

3

的事情是,BinaryFormatter的是不是真的让你的特定对象。它将一个对象图放回到内存中。对象图基本上是对象在内存中的表示;这是在对象序列化时创建的。然后,反序列化调用基本上只是将该图作为一个开放指针的对象粘贴回内存中,然后将其转换为代码的实际内容。如果输入错误,则会抛出异常。至于你的具体例子,你只是在构造一辆汽车;你只是做了一个完全相同的车。当你将它序列化到流中时,你需要存储一个精确的二进制副本。当你反序列化它时,你不需要构造任何东西。它只是将内存中的图形作为一个对象粘贴在某个指针值上,并且可以让你做任何你想做的事情。

由于Car是参考类型,您对car1!= car2的比较是真实的,因为该指针位置不同。

为什么?坦率地说,仅仅去拉二进制表示就很容易,而不必去拉各个属性和所有这些。

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

17

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

一个是为对象留出一定的内存空间,并为它作为.NET世界其余部分的一个对象进行所有必要的内务处理(注意在这个解释中有一定数量的handwaving)。

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

反序列化通过调用FormatterServices.GetUninitializedObject完成与第一步非常相似的工作,然后通过将字段的值设置为等同于序列化期间记录的字段的值来完成与第二​​步相同的操作(可能需要解串行化其他对象被称为值)。

现在,反序列化将对象放入的状态可能与任何构造函数不可能对应。最好是浪费(由构造函数设置的所有值将被覆盖),更糟的是它可能是危险的(构造函数有一些副作用)。这也可能是不可能的(只有构造函数是一个参数 - 序列化无法知道使用什么参数)。

你可以把它看作是一种特殊的构造函数,只用于反序列化(OO纯粹主义者会 - 而且应该 - 对构造函数的构思不感兴奋,我的意思是这仅仅是一个类比,如果你知道C++认为重写new的方式只要记忆力好,而且你有更好的类比,但仍然只是一个比喻)。

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

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

如果我们只关心确保在构建过程中发生的反序列化会产生一些副作用,我们可以实现IDeserializationCallback,并且在反序列化完成时调用IDeserializationCallback.OnDeserialization。至于其他与此相同的事情,在.NET中还有其他一些序列化的形式,但这就是我所知道的。你可以自己调用FormatterServices.GetUninitializedObject,但是除非你有强烈保证后续代码会将生成的对象置于有效状态的情况(也就是说,在将串行化产生的数据解序列化为对象时的情况这样做是令人担忧的并且是一种很难诊断错误的好方法。

+1

+1 - IDeserializationCallback是一个好主意。用它来初始化必要的私人领域等解决了我的问题! – womp 2011-01-28 09:10:06