2012年1月26日 星期四

序列化物件

什麼是序列化 (Serialization)

當宣告一個字串或數值變數並給定一些資料後,若須要將這些資料儲存起來,我想這沒多大問題,甚至自訂一個格式,日後還可以把它讀入相同型別的變數。 不過,如果這個變數是一個自訂的類別,例如 Employee ,或者一個 .NET 參考型別,如 Font、Hashtable、Datatable 類別等等,那你又會如何儲存呢? 這時,我們需要將物件資料轉成某個特定格式,這樣子才有辨法儲存至檔案、或者傳送給另一個程式、或者透過網路傳輸。 這樣子的一個轉換過程,就稱為 序列化 (Serialization)

序列化可分成二個階段:

  • 序列化 (Serializing) : 將一個 object 轉換成線性的 byte[] 資料,以方便資料的儲存與傳輸。
  • 還原序列化 (Deserializing) : 將先前序列化的資料轉換成 object。

一個序列化後的物件可以被儲存下來,等到要用的時候,只須要呼叫還原序列化的方法,這個物件就可以被完整的重建。 同樣的,將一個物件序列化成資料流後,就可以透過網路傳送給遠端的另一個程式,然後再由遠端的另一個程式進行還原序列化。

.Net 的序列化是實作在 System.Runtime.Serialization 命名空間裡,下表是三種系統提供的序列化方法,另外也可以自訂序列化方法。

  1. BinaryFormatter
    • 以二進位格式序列化和還原序列化物件。
    • 這個序列化物件最有效率的方法。
    • 它所序列化的資訊只能讓 .NET Framework 為主的應用程式讀取。
    • 命名空間 System.Runtime.Serialization.Formatters.Binary
  2. SoapFormatter :以 SOAP 格式序列化和還原序列化物件,或連接之物件的整個圖形。
  3. XmlSerializer
    • 將物件序列化成為 XML 文件,以及從 XML 文件將物件還原序列化。
    • 是一種公開的標準,以文字為主的格式,和各種平台相容性最高。
    • 只能序列化公用(public)的屬性與欄位,不能序列化私用資料 (private)。
    • 只能用於序列化物件,不能序列化物件圖形(object graph)。
    • 命名空間 System.Xml.Serialization

選擇何種序列化

若確認所有使用已序列化資料的用戶端都是.NET Framework 應用程式時,才應該選擇 BinaryFormatter。例如同一支程式的寫入與讀取,使用 BinaryFormatter 就是最好的選擇。 若是會尤其他應用程式讀取你的序列化資料,或是該資料可能透過網路傳輸時,則最好使用 SoapFormatter 。 另外要注意的是,使用 SoapFormatter 產生的資訊,空間會是 BinaryFormatter 的3~4倍。

如何使用 BinaryFormatter

BinaryFormatter 建構子:

public BinaryFormatter();
public BinaryFormatter(ISurrogateSelector selector, StreamingContext context);

Serialize and Deserialize 方法:

public void Serialize(Stream serializationStream, object graph);
public void Serialize(Stream serializationStream, object graph, Header[] headers);

public object Deserialize(Stream serializationStream);
public object Deserialize(Stream serializationStream, HeaderHandler handler);

序列化的步驟:

string data = "Hello World.";

FileStream fs = new FileStream("String.BIN", FileMode.Create);      // 建立 FileStream ,用以儲存序列化後的資料
BinaryFormatter bFormat = new BinaryFormatter();                    // 建立 BinaryFormatter 物件
bFormat.Serialize(fs, data);                                        // Serialize :序列化物件
 
fs.Close();
DateTime data = System.DateTime.Now;

FileStream fs = new FileStream("Date.BIN", FileMode.Create);
BinaryFormatter bFormat = new BinaryFormatter(); 
bFormat.Serialize(fs, System.DateTime.Now);

fs.Close();

還原序列化的步驟:

FileStream fs = new FileStream("String.BIN", FileMode.Open);    // 建立 FileStream 開啟序列化資料
BinaryFormatter bFormat = new BinaryFormatter();                // 建立 BinaryFormatter 物件
string data = (string) bFormat.Deserialize(fs);                // Deserialize :反序列化資料流中的資料,並轉成正確型別
fs.Close();     
Console.WriteLine(data);

如何使用 SoapFormatter

在使用 SoapFormatter 之前,必需先手動在專案中加入 System.Runtime.Serialization.Formatters.Soap.dll 這個組件的參考 (BinaryFormatte 預設已加入)。 接著其使用方法和 BinaryFormatter 都是一樣的,只是換成 SoapFormatter 類別即可。 SoapFormatter 執行序列化的效率較低,但比較具有相互運作性,它是 BinaryFormatter 類別的替代方案。 若需要可攜性,請使用 SoapFormatter ,若需要最大的效能,則使用 BinaryFormatter

NameValueCollection myDic = new NameValueCollection();

myDic.Add("apple", "a fruit ");
myDic.Add("book", "a long written or printed literary composition ");
myDic.Add("cat", "an animal ");

FileStream fs = new FileStream("NameValueCollection.SOAP", FileMode.Create);
SoapFormatter sFormat = new SoapFormatter();
sFormat.Serialize(fs, myDic);

fs.Close();
FileStream fs = new FileStream("NameValueCollection.SOAP", FileMode.Open);
SoapFormatter sFormat = new SoapFormatter();
NameValueCollection myDic = (NameValueCollection)sFormat.Deserialize(fs);
fs.Close();

foreach (string key in myDic) 
{
    Console.WriteLine("{0} : {1}", key, myDic[key]); 
}
//apple : a fruit 
//book : a long written or printed literary composition 
//cat : an animal 

如何建立可被序列化的類別

以下是使用序列化時要注意的事項:

  • 1. 一個類別,若要可被序列化及還原序列化,該類別需加上 Serialzable 屬性。
  • 2. 若沒有其他特別的標示,該類別的所有成員都會被序列化。
  • 3. 若有不需被序列化的成員,可對成員加上 NonSerialized 屬性。例如暫存或計算而得的成員,這類值沒有序列化的必要。
  • 4. 還原序列化時,若要自動讓程式對 NonSerialized 的成員進行初始化,可使用 IDeserializationCallback 介面,並實作 OnDeserialization 方法。
  • 5. 若類別因版本上有所的差異,在還原序列化時,會產生例外狀況。

如下面例子中的 Grade 自訂類別,若第一版只有 math, english 二個成員,但是第二版加了 chinese 這個新成員,若直接針對前版的序列化資料執行還原序列化,會產生例外狀況。有二個方法可以避免這個限制:

  • 自訂序列化(custom serialization),讓它可以匯入舊版的序列化物件。
  • 套用 OptionalField 屬性,以解決相容性問題。
[Serializable] //宣告成可序列化
public class Grade : IDeserializationCallback //--> 這個介面主要功能在當執行完 Deserialization 之後,通知類別
{
    public string name;
    public int math;
    private int english;
    [OptionalField] public int chinese;     //[ OptionalField ]-->因序列化的資料是舊版格式,沒有這個欄位,設成[OptionalField],提供版本的相容性。
    [NonSerialized] public int total;       //[ NonSerialized ]-->定義這個項目不要序列化。所以,在還原序列化時這個欄位也不會被還原。常用放暫時性欄位。

    public Grade(string _name, int _math, int _english, int _chinese)
    {
        name = _name;
        math = _math;
        english = _english;
        chinese = _chinese;
        total = _math + _english + _chinese;
    }

    public string result()
    {
        if ((total) > 180)
            return "pass";
        else
            return "fail";
    }

    //實作 IDeserializationCallback.OnDeserialization ,這樣子當 Deserialization 之後,會自動執行這段程式碼
    public void OnDeserialization(object sender)
    {
        //因為chinese為[OptionalField]屬性,屬舊版沒有的欄位,若有必要,可以在此做初始化設定
        if (chinese == 0)
            chinese = 60;

        //因為total為加總欄位,沒有必要被序列化,所以當初被設定為 [NonSerialized],
        //但是還原後,這個欄位是有意義的,所以可以在此重新計算
        total = math + english + chinese;
    }
}
Grade grade = new Grade("vito", 80, 90, 30);

FileStream fs = new FileStream("Class.Soap", FileMode.Create);
SoapFormatter sFormat = new SoapFormatter();
sFormat.Serialize(fs, grade);
fs.Close();
FileStream fs = new FileStream("Class.Soap", FileMode.Open);
SoapFormatter bFormat = new SoapFormatter();
Grade grade = (Grade)bFormat.Deserialize(fs);
fs.Close();
Console.WriteLine(grade.result());  //還原序列化後,試試呼叫 result 這個方法

1 則留言: