2010-08-17 48 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