.NET 筆記:序列化

摘要:.NET 筆記:序列化

作者:蔡煥麟
日期:Nov-5-2005


簡介

序列化(serialization)是一種將物件的狀態經過一道轉換的程序,儲存至另一個儲存媒體的技術;反序列化則是相反的程序,也就是將之前儲存的序列化資料還原成物件。

例如,你可以把一塊冰塊融化成水,然後透過水管流到瓶子裡的過程看成是序列化,而將瓶子裡的水透過水管倒回製冰盒凝結成冰塊的過程,則可視為反序列化。

用途

序列化/反序列化的主要用途,在於將物件在執行時期的狀態(在記憶體中的樣子)予以保留,所謂的保留,通常就是儲存在其他儲存媒體,例如:硬碟,以便下次(例如:重新開機後)可以再透過反序列化的程序恢復物件的狀態。

序列化的方法

序列化的方法主要有三種:

  1. 使用串流物件自行撰寫程式儲存物件的狀態;

  2. 使用 Serializable 特徵項與 formatter 類別;

  3. 使用 XmlSerializer 類別。

第一種方式最麻煩,這裡不多作介紹,如果您有興趣的話,可以試試看自行用 StreamReader/StreamWriter 或 BinaryReader/BinaryWriter 將物件的狀態儲存成文字格式或二進位格式的檔案,您會發現這種方式要寫很多程式碼,而且日後的維護也挺麻煩。

以下介紹其他兩種方式。

方法一:使用 Serializable 特徵項與 formatter 類別

這是最簡單的方式,主要的步驟為:

  1. 將類別以 [Serializable] 特徵項標示為可序列化的。注意使用此特徵項時,程式必須匯入命名空間:System.Runtime.Serialization。

  2. 使用 formatter 將物件序列化至特定的串流。

Formatter 物件會在稍後介紹,先來看看如何將類別標示為可序列化的,參考以下範例:

using System.Runtime.Serialization;
....
[Serializable]
public class Employee { public string name; public int age; private double salary; // 注意這個成員是 private }

類別已經標示為可序列化之後,接著便可使用 formatter 物件將物件進行序列化或反序列化。

Formatter 物件的任務有二:

  1. 把物件的欄位資料進行序列化,包括 private, protected, public 成員,都可以被序列化,而且物件中的巢狀物件也會被序列化,因此這種方式又稱為深層序列化

  2. 把各種型態的資料轉換成 byte 層級的資料。

.NET framework 提供兩種 formatter 類別:

  • BinaryFormatter

  • SoapFormatter

BinaryFormatter 會將物件的狀態資料轉換成二進位格式的資料;SoapFormatter 則是將物件的狀態資料轉換成 SOAP 的文字格式,雖然 SOAP 是 Web Services 的標準,但 SoapFormatter 跟 Web Services 並沒有什麼關係,只是它序列化的結果方便人閱讀而已。

這兩種 formatter 都實作了 IFormatter 介面:

public interface IFormatter
{
  void Serialize(Stream serializationStream, object graph);
  object Deserialize(Stream serializationStream);

  // 省略其他不常用的成員
}

其中 Serialize 方法就是將物件序列化至指定的串流,而 Deserialize 方法則是反序列化。還記得前面筆者舉的冰塊的例子嗎?你可以把這裡的串流想成是那條水管。

BinaryFormatter 範例

以下示範用 BinaryFormatter 將前面定義的 Employee 類別的物件資料序列化:

using System.Runtime.Serialization.Formatters.Binary
....
private void btnSerialize_Click(object sender, System.EventArgs e)
{
  // 示範如何將物件利用 BinaryFormatter 序列化到檔案中
  Employee emp = new Employee();
  emp.name = "Will Tsai";
  emp.age = 30;

  FileStream fs = new FileStream(@"c:\0\test.bin", FileMode.Create);

  IFormatter formatter = new BinaryFormatter();
  formatter.Serialize(fs, emp);

  fs.Close();
}

以下是反序列化的範例:

private void btnDeserialize_Click(object sender, System.EventArgs e)
{
  // 示範如何利用 BinaryFormatter 進行反序列化
  FileStream fs = new FileStream(@"c:\0\test.bin", FileMode.Open);

  IFormatter formatter = new BinaryFormatter();

  // 注意不需要 new Employee 物件,反序列化時就會建立物件實體
  Employee emp = (Employee) formatter.Deserialize(fs);

  MessageBox.Show(emp.name);

  fs.Close(); 
}

SoapFormatter 範例

以下是使用 SoapFormatter 序列化物件的範例:

using System.Runtime.Serialization.Formatters.Soap
....
private void btnSerialize_Click(object sender, System.EventArgs e)
{
  // 示範如何將物件利用 SoapFormatter 序列化到檔案中
  Employee emp = new Employee();
  emp.name = "Will Tsai";
  emp.age = 30;

  FileStream fs = new FileStream(@"c:\0\test.soap", FileMode.Create);

  IFormatter formatter = new SoapFormatter();
  formatter.Serialize(fs, emp);

  fs.Close();
}

序列化產生的檔案內容如下:

<SOAP-ENV:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
    xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" 
    xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" 
    xmlns:clr="http://schemas.microsoft.com/soap/encoding/clr/1.0" 
    SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> 
  <SOAP-ENV:Body>
    <a1:Employee id="ref-1" xmlns:a1="...(略)">
      <name id="ref-3">Will Tsai</name> 
<age>30</age>
<salary>0</salary>
</a1:Employee> </SOAP-ENV:Body> </SOAP-ENV:Envelope>

反序列化的寫法跟前面 BinaryFormatter 的範例類似,這裡就不列出了。

注意:在使用 SoapFormatter 時,專案必須先加入組件參考:System.Runtime.Serialization.Formatters.Soap。

指定某些成員不要序列化

由於 Serializable 特徵項會告知 formatter 物件將所有資料成員序列化,可是有時候我們會希望某些成員不要被序列化,例如:動態計算或者暫時性的資料成員,這時後可以在資料成員前面加上 NonSerialized 特徵項,例如:

[Serializable]
public class Employee 
{
  public string name;
  [NonSerialized] public int age;
  private double salary; // 注意這個成員是 private
}

方法二:使用 XmlSerializer 類別

使用 XmlSerializer 類別序列化物件時,被序列化的物件,其類別無須加上 Serializable 特徵項。以下是序列化範例:

using System.Xml.Serialization;
....
private void btnXmlSerialize_Click(object sender, System.EventArgs e)
{
  // 示範如何利用 XmlSerializer 序列化物件
  Employee emp = new Employee();
  emp.name = "Will Tsai";
  emp.age = 30;

  FileStream fs = new FileStream(@"c:\0\test.xml", FileMode.Create);

  XmlSerializer xs = new XmlSerializer(typeof(Employee));
  xs.Serialize(fs, emp);

  fs.Close();
}

序列化的檔案內容如下:

<?xml version="1.0"?>
<Employee xmlns:xsd="...(略)">
  <name>Will Tsai</name>
<age>30</age>
<gender>false</gender>
</Employee>

跟前面用 SoapFormatter 序列化的結果比較看看,兩者有何不同?

使用 XmlSerializer 序列化 Employee 物件的結果少了 salary 這個欄位,因為這種方法無法序列化 private、protected 層級的資料成員。此外,XmlSerializer 也不會針對物件內部的巢狀物件作處理,這種方式稱為淺層序列化

注意:Employee 類別的存取範圍必須是 public,否則在使用 XmlSerializer 執行序列化時會發生異常(exception)。

以下是反序列化範例:

private void btnXmlDeserialize_Click(object sender, System.EventArgs e)
{
  // 示範如何利用 XmlSerializer 序列化物件
  FileStream fs = new FileStream(@"c:\0\test.xml", FileMode.Open);

  XmlSerializer xs = new XmlSerializer(typeof(Employee));
Employee emp = (Employee) xs.Deserialize(fs);
MessageBox.Show(emp.name); fs.Close(); }

結語

本文介紹了兩種序列化即反序列化物件的作法,使用 Serializable 搭配 formatter 的作法是最簡單的,而且可以處理深層序列化(可序列化巢狀物件),由於 formatter 是透過 Reflection 技術取得型別資訊,因此資料成員不管是 public、protected、還是 private,都可以序列化。另一種是使用 XmlSerializer 物件執行淺層的序列化,這種方式只會處理 public 資料成員。

有些進階議題並沒有在本文中提及,例如:IDeserializationCallback 和 ISerializable,這兩個介面都可以讓你更彈性的處理序列化的動作,如果有興趣進一步研究,可以用 Google 尋找相關資料。