持久化是類型的一個核心特性,有時我們需要通過不同的方式傳輸和創建同一個對象,例如需要通過網絡傳輸對象,或者需要將對象信息存儲到文本文件或者XML文件中,這時,如何能夠保持對象的狀態,並在將來使用時,可以準確的還原到原來的狀態,是非常重要的。
.NET可以使用序列化的方式來持久化對象,我們在可能的情況下,應該將類型定義爲可以序列化的。序列化時,我們可以使用Serializable特性。
我們來看下面的簡單示例,將對象信息存儲到XML文件中。
首先定義兩個可以序列化的類型。
[Serializable]
public class Employee
{
private PersonName m_Name;
public PersonName Name
{
get { return m_Name; }
set { m_Name = value; }
}
private string m_strAddress;
public string Address
{
get { return m_strAddress; }
set { m_strAddress = value; }
}
public Employee()
{
m_Name = new PersonName();
m_strAddress = string.Empty;
}
public Employee(PersonName name, string address)
{
m_Name = name;
m_strAddress = address;
}
public override string ToString()
{
return string.Format("Name : {0} {1}, Address : {2}", m_Name.FirstName, m_Name.LastName, m_strAddress);
}
}
[Serializable]
public class PersonName
{
private string m_strFirstName;
public string FirstName
{
get { return m_strFirstName; }
set { m_strFirstName = value; }
}
private string m_strLastName;
public string LastName
{
get { return m_strLastName; }
set { m_strLastName = value; }
}
public PersonName()
{
m_strFirstName = string.Empty;
m_strLastName = string.Empty;
}
public PersonName(string firstName, string lastName)
{
m_strFirstName = firstName;
m_strLastName = lastName;
}
}
下面是測試方法,包含了序列化和反序列化的過程。
private static void Test()
{
Employee emp = new Employee(new PersonName("Wing", "Lee"), "BeiJing");
Console.WriteLine("Ouput emp info before serialize:");
Console.WriteLine(emp.ToString());
XmlSerializer serializer = new XmlSerializer(typeof(Employee));
StreamWriter writer = new StreamWriter("emp.xml");
serializer.Serialize(writer, emp);
writer.Close();
Console.WriteLine("Serialization Success.");
StreamReader reader = new StreamReader("emp.xml");
XmlSerializer desrializer = new XmlSerializer(typeof(Employee));
object o = desrializer.Deserialize(reader);
if (o is Employee)
{
Console.WriteLine("Ouput emp info after deserialize:");
Console.WriteLine((o as Employee).ToString());
}
}
上述代碼的執行結果如下所示。
可以看到序列化前和序列化後的對象信息,被完全一致的反應出來,這說明,序列化確實實現了對象的持久化。
有以下兩個問題需要注意:
- 對於可以序列化的類型,必須提供沒有參數的構造函數,上述代碼中,如果Employee類型和PersonName類型沒有顯示的提供默認構造函數,那麼程序在編譯時,就會報錯,提示在不提供沒有參數的構造函數的情況下,無法執行序列化操作。
- 出於性能的考慮,我們可以不用將類型全部內容都置爲可序列化。
C#在序列化時,可以採用上面代碼中寫的XmlSerializer的方式,也可以採用BinaryFormatter的方式,如果採用XmlSerializer的方式,那麼NonSerialized特性是不會發揮作用的,同時這種方式允許序列化中的類型包含不能序列化的類型;但是對於BinaryFormatter來說,可以使用NonSerialized特性,同時進行序列化的類型所包含的其他所有類型,都必須是可序列化的,否則就會在序列化的過程中發生異常。
我們來看以下的代碼,使用Formatter的正常方式。
private static void TestWithFormatter()
{
Employee emp = new Employee(new PersonName("Wing", "Lee"), "BeiJing");
Console.WriteLine("Ouput emp info before serialize:");
Console.WriteLine(emp.ToString());
BinaryFormatter serializer = new BinaryFormatter();
Stream stream = File.Open("emp.txt", FileMode.Create);
serializer.Serialize(stream, emp, null);
stream.Close();
Console.WriteLine("Serialization Success.");
stream = File.Open("emp.txt", FileMode.Open);
BinaryFormatter deserializer = new BinaryFormatter();
object o = deserializer.Deserialize(stream, null);
stream.Close();
if (o is Employee)
{
Console.WriteLine("Ouput emp info after deserialize:");
Console.WriteLine((o as Employee).ToString());
}
}
我們來修改一下Employee類型的代碼,將m_strAddress字段用NonSerialized特性進行修飾,然後執行上述Test()方法和TestWithFormatter()方法,其中,Test()方法的執行結果是不會改變的;但是TestWithFormatter()方法的執行結果如下所示。
我們來修改一下Employee類型的代碼,將m_strAddress字段用NonSerialized特性進行修飾,然後執行上述Test()方法和TestWithFormatter()方法,其中,Test()方法的執行結果是不會改變的;但是TestWithFormatter()方法的執行結果如下所示。
[Serializable]
public class Employee : IDeserializationCallback
{
private PersonName m_Name;
public PersonName Name
{
get { return m_Name; }
set { m_Name = value; }
}
[NonSerialized]
private string m_strAddress;
public string Address
{
get { return m_strAddress; }
set { m_strAddress = value; }
}
public Employee()
{
m_Name = new PersonName();
m_strAddress = string.Empty;
}
public Employee(PersonName name, string address)
{
m_Name = name;
m_strAddress = address;
}
public override string ToString()
{
return string.Format("Name : {0} {1}, Address : {2}", m_Name.FirstName, m_Name.LastName, m_strAddress);
}
public void OnDeserialization(object sender)
{
this.Address = "Vernus";
}
}
上述代碼實現了IDeserializationCallBack接口,在接口的OnDeserialization()方法中,我們對Address屬性進行重新賦值,在代碼修改後,TestWithFormatter()方法的執行結果如下圖所示。
我們在編碼的過程中,可能在將對象序列化後,又修改了類型的結構,這時,單純採用Serialzable特性,是不能對修改進行區分的,甚至可能會引發異常。這時,我們可以實現ISerializable接口,來定製序列化過程。
我們來看下面的代碼。
[Serializable]
public class Employee : IDeserializationCallback, ISerializable
{
private PersonName m_Name;
public PersonName Name
{
get { return m_Name; }
set { m_Name = value; }
}
[NonSerialized]
private string m_strAddress;
public string Address
{
get { return m_strAddress; }
set { m_strAddress = value; }
}
public Employee()
{
m_Name = new PersonName();
m_strAddress = string.Empty;
}
public Employee(PersonName name, string address)
{
m_Name = name;
m_strAddress = address;
}
private Employee(SerializationInfo info, StreamingContext context)
{
Name = info.GetValue("name", typeof(PersonName)) as PersonName;
Address = info.GetString("address");
}
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.AddValue("name", Name);
info.AddValue("address", Address);
}
public void OnDeserialization(object sender)
{
this.Address = "Vernus";
}
public override string ToString()
{
return string.Format("Name : {0} {1}, Address : {2}", m_Name.FirstName, m_Name.LastName, m_strAddress);
}
}
上述代碼在執行TestWithFormatter()方法後的結果沒有發生變化。我們在ISerializable接口的GetObjectData()方法中,以鍵/值對的方式將類型中的數據項進行存儲,同時添加了一個構造函數,用於得到反序列化後對象中各數據項的值。
一般情況下,我們對實現了ISerializable接口的類聲明爲sealed,這樣可以免於被繼承,因爲繼承後的子類,還要針對自身的數據情況,對GetObjectData()方法進行擴充,比較複雜。
編程時要注意,從序列化流中寫入和讀取數據的順序必須一致。
總結:.NET框架爲對象序列化提供了一個簡單、標準的算法。如果我們的類型需要持久化,那就應該遵循標準的實現。如果我們不爲類型添加序列化支持,那麼其他使用我們類型的類也就不能支持序列化。我們所做的工作應該儘可能的使類型的使用者更加方便。如果可以,應該使用默認的方式來支持序列化;如果默認的Serializable特性不能夠滿足要求,則應該實現ISerializable接口。