序列化

     序列化這個東西,從很久以前就誕生了,以前很多編程語言中都有它的身影。所謂序列化,無非就是將程序內存中的數據,按照一定的組織形式,轉化爲順序而連續的數據,以方便於網絡傳輸和存儲。隨着XML的廣泛應用,又出現了將數據轉化爲xml文檔格式的序列化,我們這裏稱之爲XML序列化。

本文中,將要講述.Net編程中的普通序列化和XML序列化功能。其實對於.Net,有兩種序列化方式:

1) 使用平臺本身提供的自動序列化功能,此功能並不需要被序列化的類型實現任何接口或者編寫任何更多的附加代碼。
2) 通過實現接口來自定義序列化功能,在接口方法中寫入代碼來實現序列化。

    反射(Reflection)是.Net一個偉大的創舉,這讓我們的代碼變得更加“智能”,它的出現也使得自動序列化變得可能。其實在大多數情況下,我們使用自動序列化已經足夠滿足我們的需求,但是可能在某些特殊情況下,我們需要實現相關的接口來完成自定義的序列化功能。

序列化是如何工作的

    爲了讓對序列化這個概念並不熟悉的讀者有一個具體的認識,我寫了下面的代碼來演示一個使用序列化的程序如何工作的。
[Serializable]//該標籤表示User類型可序列化 
class User
{
public string name;
public int age;
public override string ToString()
{
return "Name: " + name + "/tAge: " + age.ToString();
}
}
static void Main(string[] args)
{
//申明一個二進制序列化器
BinaryFormatter formatter = new BinaryFormatter();
FileStream fs = File.Open("User.dat", FileMode.Create);

User user=new User();//創建一個用戶對象
user.name="張三";
user.age=20;

formatter.Serialize(fs,user);//序列化至文件
fs.Close();//關閉

fs = File.Open("User.dat", FileMode.Open);//打開文件
User user2 = formatter.Deserialize(fs) as User;//反序列化至內存對象
Console.Write(user2);//打印反序列化的結果
Console.ReadKey();
}

    運行結果:
   Name: 張三 Age: 20

    我們可以看到,BinaryFormatter(二進制序列化器)能夠將user對象序列化到文件user.dat中,然後又通過反序列化將其從user.dat讀取到user2對象中,整個過程中數據沒有發生丟失或者改變。

    我們知道文件在邏輯上是一個字節跟着一個字節存儲的,是一個順序的存儲方式,所以這樣將內存中的數據轉化爲一個串行的數據流的過程,我們可以稱之爲“序列化”;而這一過程的逆過程,就叫做反序列化了。

   除了二進制序列化器之外,系統還提供了很多其他類型的Fomatter,比如我們在項目引用之中添加了System.Runtime.Serialization.Formatters.Soap之後,還可以找到一個SoapFormatter,專門用於將對象序列化爲Web Service需要的Soap格式。當然,我們也可以書寫自己的Formatter,只需要將自己的格式化器類型實現IFormatter接口就可以了。 

利用ISerializable進行自定義序列化

    上面那個例子中,是使用BinaryFormatter進行自動序列化,除了給User類增加了【Serializable】標籤之外,沒有書寫任何代碼,BinaryFormatter就自動將User類型的所有信息(公共字段和屬性)進行了序列化和反序列化,這大大減少了我們進行代碼開發的工作量,但是這種自動帶來方便的同時,也使得我們無法對其序列化的過程進行任何干預和控制,而當我們想要對序列化的具體內容進行改變的時候,ISerializable就有它的用武之地了。

   下面是ISerializable的定義,但是我們發現它只定義了一個接口函數“GetObjectData”,這個是序列化的時候使用的,而進行反序列化的函數卻沒有定義,這是爲什麼呢?從MSDN中看到,原來是實現ISerializable的類型如果需要進行反序列化,必須還定義一個以SerializationInfo和StreamingContext爲參數的公共構造函數,各種格式化器將使用這個公共的構造函數來反序列對象。

namespace System.Runtime.Serialization 
{
// Summary:
// Allows an object to control its own serialization and deserialization.
[ComVisible(true)]
public interface ISerializable
{
// Summary:
// Populates a System.Runtime.Serialization.SerializationInfo with the data
// needed to serialize the target object.
//
// Parameters:
// context:
// The destination (see System.Runtime.Serialization.StreamingContext) for this
// serialization.
//
// info:
// The System.Runtime.Serialization.SerializationInfo to populate with data.
//
// Exceptions:
// System.Security.SecurityException:
// The caller does not have the required permission.
void GetObjectData(SerializationInfo info, StreamingContext context);
}
}

    爲了便於理解自定義序列化功能的必要性,我們這裏給User類又增加了兩個公共成員:temp和saveTime,其中temp是一個臨時變量,並不需要進行序列化,以便節省網絡帶寬或者存儲空間;而saveTime則標識這個對象上次被序列化的時間。這樣一來,我們序列化的需求就發生了改變,一個成員不需要序列化,而另外一個成員序列化的值是需要在序列化的時候才能夠決定的。

    下面的代碼演示瞭如何實現自定義序列化的功能

[Serializable]//該標籤表示User類型可序列化 
class User:ISerializable
{
public string name;
public int age;
public int temp=0;
public DateTime saveTime=DateTime.MinValue;
public override string ToString()
{
return "Name: " + name + "/tAge: " + age.ToString() +"/tTemp:"
+ temp.ToString() + "/tSaveTime:" + saveTime.ToString();
}


public User()
{

}
//這個公共構造函數必須在實現接口ISerializable之後被實現,否則將無法被反序列化
public User(SerializationInfo info, StreamingContext context)
{
//將各參數值取出來
name = info.GetString("name");
age = info.GetInt32("age");
saveTime = info.GetDateTime("saveTime");

}


#region ISerializable Members

public void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.SetType(typeof(User)); //設置對象類型
//添加序列化的數據
info.AddValue("name", name);
info.AddValue("age", age);
// Temp 不需要被序列化,所以我們並未將其增加到info
info.AddValue("saveTime", DateTime.Now); //saveTime取當前的時間,以便滿足我們的需求
}

#endregion
}

XML序列化

    XML文檔擁有諸多的優點,可以按照層次結構存儲數據,可以直接手工讀寫,將數據序列化爲XML文檔顯然更加適合網絡傳輸和多個應用程序之間的交互;利用XML存儲的配置文件也能夠進行手工修改。不多說了,我們還是演示一下XML序列化是如何被實現的,下面的代碼使用System.Xml.Serialization.XmlSerializer對上個例子裏面的User類型進行序列化。

static void Main(string[] args) 
{
StringBuilder sb = new StringBuilder();
XmlWriter xw = XmlWriter.Create(sb);
XmlSerializer xs = new XmlSerializer(typeof(User));

User user = new User();//創建一個用戶對象
user.name = "張三";
user.age = 20;
xs.Serialize(xw, user);

Console.Write(sb.ToString());//打印序列化產生的xml文檔
Console.ReadKey();
}

輸出結果是:

<?xml version="1.0" encoding="utf-16"?> 
<User xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<name>張三</name>
<age>20</age>
<temp>0</temp>
<saveTime>0001-01-01T00:00:00</saveTime>
</User>


利用Attributes對XML序列化進行控制

    System.Xml.Serialization名字空間中定義了大量的Attributes來對XML的序列化過程進行干預。其中比較常用的有XmlRoot、 XmlElement、 XmlAttribute和XmlIgnore等等。下面我們對User類型的XML序列化進行一定的定製,將XML根節點設置爲UserInfo,用戶名字段通過Element方式進行存儲,年齡字段通過XML屬性方式進行存儲,而Temp字段則不希望被序列化,我們來打上這些標籤,看看有什麼效果。

[XmlRoot("UserInfo")]//重新命名根節點 
public class User
{
[XmlElement("UserName")]//重新命名
public string name;
[XmlAttribute("Age")] //設置爲屬性
public int age;
[XmlIgnore] //不對Temp進行序列化
public int temp = 0;
public DateTime saveTime = DateTime.MinValue;
}

   運行結果:

<?xml version="1.0" encoding="utf-16"?> 
<UserInfo xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" Age="20">
<UserName>張三</UserName>
<saveTime>0001-01-01T00:00:00</saveTime>
</UserInfo>

    我們看到,通常利用Attributes標籤,已經能讓Xml序列化器按照我們的方式來進行序列化了,當然如果這些都無法滿足你的要求的話,那麼還有一種辦法,那就是實現IXmlSerializable接口。
利用IXmlSerializable接口來對序列化的過程進行完全的控制
    IXmlSerializable接口的定義如下,其中ReadXml是用來進行反序列化的,而WriteXml則是進行序列化的,GetSchema函數可以不實現。

// Summary: 
// Provides custom formatting for XML serialization and deserialization.
public interface IXmlSerializable
{
XmlSchema GetSchema();
void ReadXml(XmlReader reader);
void WriteXml(XmlWriter writer);
}

   其使用方法很簡單,這裏不再贅述代碼。在序列化的時候,只需在WriteXml方法中,將本對象的屬性寫入到writer之中;而反序列化的時候,就是在ReadXml方法中,將Xml中的數據讀出,並且賦給各成員變量。 

序列化的對象不要循環引用

    假如我們我們在爲User類型增加下面這樣一個成員變量:

public User friend=null;

   並且將main函數中部分代碼修改如下,會出現什麼樣的效果呢?

User user = new User();//創建一個用戶對象 
user.name = "張三";
user.age = 20;


User user2 = new User();//創建一個用戶對象
user2.name = "李四";
user2.age = 20;

user.friend = user2; //User2是User的朋友
user2.friend = user; //User也是User2的朋友

xs.Serialize(xw, user);//這裏會報錯

    顯然序列user對象的時候會出現運行時錯誤,因爲user字段中包含了user2,而user2的字段中又包含了user,這樣序列化的時候就會形成循環,永無止境了。幸好我們的.Net Fraemwork相當的健壯,它並不會陷入到死循環中,但是會拋出一個異常,報告出這個錯誤。所以我們在使用序列化的時候,應該避免這種情況,我們可以採用如下方式:
爲“Friend”字段加上[XmlIgnore]來使friend字段在序列化的過程中被忽略。
    如果我們確實需要friend的信息,我們可以爲其增加一個新的屬性叫做”FriendName”,並且將其包括在序列化中即可。

總結

    曾幾何時,實現將運行時的數據保存到文件中並再讀出來的功能是多麼痛苦的一件事情,而制定專用的通信協議並且將需要的數據轉化網絡流也是多麼傷透腦筋的事情,如今,dotNet平臺已經給我們提供了強大的序列化器,是的我們再完成這些工作的時候事半功倍,無論你現在是否目前需要序列化的功能,好好地掌握它將會讓你受益無窮。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章