C#之你懂得的序列化/反序列化
前言:寫此文章一方面是為了鞏固對序列化的認識,另一方面是因為本人最近在面試,面試中被問到“為什么要序列化”。雖然一直在使用,自己也反復的提到序列化,可至于說為什么要序列化,還真的沒想過,所以本文就這樣產生了。
序列化是將一個對象轉換成一個字節流的過程。反序列化是將一個字節流轉換回對象的過程。在對象和字節流之間轉換是很有用的一個機制。(當然這個還不能回答它的實際用處)
舉點例子:
應用程序的狀態可以保存到一個磁盤文件或數據庫中,并在應用程序下次運行時恢復。比如ASP.NET就是利用系列化和反序列化保存和恢復回話狀態。
一組對象可以輕松復制到系統的剪切板,然后再粘貼到其他的地方(應用程序)。
一組對象可克隆并放到其他地方作為備份。
一組對象可以通過網絡發送給另一臺機器上運行的進程(比如Remoting)。
除了上述的幾個場景,我們可以將系列化得到的字節流進行任意的操作。
一、序列化、反序列化快速實踐
[Serializable]
class MyClass
{
public string Name { get; set; }
}
一個自定義類,切記需要加上[Serializable]特性(可應用于class、struct、enum、delegate)。
private static MemoryStream SerializeToMemoryStream(object objectGraph)
{
//一個流用來存放序列化對象
var stream = new MemoryStream();
//一個序列化格式化器
var formater = new BinaryFormatter();
//將對象序列化到Stream中
formater.Serialize(stream, objectGraph);
return stream;
}
private static object DeserializeFromMemory(Stream stream)
{
var formater = new BinaryFormatter();
return formater.Deserialize(stream);
}
SerializeToMemoryStream為序列化方法,此處通過BinaryFormatter類將對象序列化到MemoryStream中,然后返回Stream對象。
DeserizlizeFromMemory為反序列化方法,通過傳入的Stream,然后使用BinaryFormatter的Deserialize方法反序列化對象。
除了可以使用BinaryFormatter進行字節流的序列化,還可以使用XmlSerializer(將對象序列為XML)和DataContratSerializer。
Serialize的第二個參數是一個對象的引用,理論上應該可以是任何類型,不管.net的基本類型還是其他類型或者是我們的自定義類型。如果是對象和對象的引用關系,Serizlize也是可以一直序列化的,而且Serialize會很智能的序列化每個對象都只序列化一次,防止進入無限循環。
P.S. 1.Serialze方法其實可以將對象序列化為Stream,也就意味著不僅可以序列化為MemoryStream,還可以序列化為FIleStream或者是其他繼承自Stream的類型。
2.除了上述的將一個對象序列化到一個Stream,也可以將多個對象序列化中,還是調用Serialize方法,第二個參數為不同的對象即可;在反序列化的時候同樣的方法,只不過 強轉的類型指定為需要的即可。
序列化多個對象到Stream:
MyClass class1 = new MyClass();
MyClass2 class2=new MyClass2();
formater.Serialize(stream,class1);
formater.Serialize(stream,class2);
從Stream中反序列化多個對象:
MyClass class1 =(MyClass) formater.Deserialize(stream);
MyClass1 class2 = (MyClass1)formater.Deserialize(stream);
二、控制序列化和反序列化
如果給類添加了SerializeAttribute,那么類的所有實例字段(private、protected、public等)都會被序列化。但是,有時候類型中定義了一些不應序列化的實例字段。
一般情況下,以下兩種情況不希望序列化字段:
字段含有反序列化后變得無效的信息。例如,假定一個對象包含到一個Windows內核對象(如文件、進程、線程、事件等),那么在反序列化到另一個進程或另一臺機器之后,就會失去意義。
字段含有很容易計算的信息。在這種情況下,要選出那些無需序列化的字段,減少需要傳輸的數據,從而增強應用程序的性能。
使用NonSerializedAttribute特性來指明哪些字段無需序列化。
[NonSerialized]
private string _name;
p.s.[NoSerialized] 僅僅能添加在字段,或者是沒有get和set訪問器屬性上,對于有get和set這樣的屬性使用是不行的。沒關系使用[ScriptIgnore]特性標識屬性則可以忽略JSON這樣的序列化、使用[XmlIgnoreAttribute]特性標識屬性則可以忽略XmlSerializer的序列化操作。
雖然使用NonSerizlized特性可以使字段不被序列化,但是在序列化或者反序列化的時候往往都會把值清空,或者是沒有一些希望的默認值,還好我們可以使用其他的特性來輔助完成。
修改下上文中的MyClass:
[Serializable]
class MyClass
{
[NonSerialized]
public string _name;
[OnDeserialized]
private void OnDeserialized(StreamingContext context)
{
_name = "Mario";
}
[OnDeserializing]
private void OnDeserializing(StreamingContext context)
{
_name = "super";
}
[OnSerializing]
private void OnSerializing(StreamingContext context)
{
_name = "listen";
}
[OnSerialized]
private void OnSerialized(StreamingContext context)
{
_name = "fly";
}
public void Print()
{
Console.WriteLine(_name);
}
}
在類中一共使用了四個特性,OnDeserialized、OnDeserializing、OnSerializing、OnSerialized,分別是反序列化后、反序列化前、序列化前、序列化后。不過,如果同時指定了OnDeserialized和OnDeserializing,那么結果應該是OnDeserialized中的邏輯;同理,如果同時指定了OnSerializing和OnSerialized,那么結果應該是OnSerialized中的邏輯。另外,在一個類中,僅僅能指定一個方法為上述中的一個特性(即OnSerialized特性只能被一個方法使用、OnSerialized特性只能被一個方法使用,其余兩個同理),否則序列化或者反序列化則會出現異常。
P.S. 這些方法通常為private的,并且參數為StreamingContext。
MyClass class1 = new MyClass();
var stream = SerializeToMemoryStream(class1);
class1.Print();
stream.Position = 0;
class1 = (MyClass)DesrializeFromMemory(stream);
class1.Print();
Console.Read();
運行上述調用可以發現,雖然我們沒有將name屬性序列化,但是在序列化/反序列化之后還是可以輸出值的,如果你同時指定了OnDeserializing和OnDeserialized或者同時指定了OnSerializing和OnSerialized,那么你會發現使用的都是后者的值,這也驗證了上述中的解釋。
有時候我們的類可能會增加字段,可是呢,我們已經序列化好的數據是舊的版本,所以在反序列化的時候就會出現異常,還好我們也有辦法,給新加的字段都增加一個OptinalFieldAttribute特性,這樣當格式化器看到該attribute應用于一個字段時,就不會因為流中的數據不包含這個字段而出現異常。
三、序列化和反序列化的原理
為了簡化格式化器的操作,在System.Runteime.Serialization中有一個FormatterServices類型。該類型只包含靜態方法,并且該類為靜態類。
Serialize步驟:
格式化器調用FormatterServices的GetSerializableMembers方法:
public static MemberInfo[] GetSerializableMembers(Type type,StreamContext context);
這個方法利用反射獲取類型的public和private實例字段(除了標識為NonSerializedAttribute的字段除外)。方法返回由MemberInfo對象構成的一個數組,其中每個元素都對應于一個可序列化的實例字段。
對象被序列化,MemberInfo對象數組傳給FormatterServices的靜態方法GetObjectData:
public static object[] GetObjectData(Object obj,MemberInfo[] members);
這個方法返回一個Object數組,其中每個元素都標識了被序列化的那個對象的一個字段的值。這個Object數組和MemberInfo數組是并行的;也就是說,Object數組中的元素0是MemberInfo數組中的元素0所標識的那個成員的值。
格式化器將程序集標識和類型的完整名稱寫入流中。
格式化器然后遍歷兩個數組中的元素,將每個成員的名稱和值寫入流中。
Deserialize步驟:
格式化器從流中讀取程序集標識和完整類型名稱。如果程序集當前沒有加載到AppDomain中,就加載它。如果程序集不能加載,則出現異常。如果程序集已經加載,格式化器將程序集標識信息和類型全名傳給FormatterServices的靜態方法GetTypeFromAssembly:
public static Type GetTypeFromAssembly(Assembly assembly, string name);
這個方法返回一個Type對象,代表要反序列化的那個對象的類型。
格式化器調用FormatterServices的靜態方法GetUninitializedObject:
public static Object GetUninitializedObject(Type type);
這個方法為一個新對象分配內存,并不為對象調用構造函數。所以,對象的所有字段都被初始化為null或者0;
格式化器現在構造并初始化一個MemberInfo數組,同樣是調用FormatterServices的GetSerializableMembers方法。這個方法返回序列化好,需要反序列化的一組字段。
格式化器根據流中包含的數據創建并初始化一個Object數組。
將對新分配的對象、MemberInfo數組以及并行Object數組的傳給FomatterServices的靜態方法PopulateObjectMembers:
public static Object PopulateObjectMembers(Object obj,MemberInfo[] members, Object [] data);
這個方法遍歷數組,將每個字段初始化成對應的值。到這里,就算反序列化結束了。
四、控制序列化/反序列化的數據
本文上述,有提到如何使用OnSerializing、OnSerialized、OnDeserializing、OnDeserialized以及NonSerialized和OptionalField特性進行控制序列化和反序列化。但是,格式化器內部使用反射,而反射的速度是比較慢的,所以增加了序列化和反序列化對象所花的時間。為了對序列化和反序列化完全的控制,并且不使用反射,那么我們的類型可以實現ISerializable接口,此接口僅僅有一個方法:
public Interface ISerializable
{
void GetObjectData(SerializationInfo info, StreamContext context);
}
一旦類型實現了此接口,所有派生類型也必須實現它,而且派生類型必須保證調用基類的GetOBjectData方法和特殊的構造器。除此之外,一旦類型實現了該接口,則永遠不能刪除它,否則會失去與派生類的兼容性。
ISerializable接口和特殊構造器旨在由格式化器使用。但是,任何代碼都可能調用GetObjectData,則可能返回敏感數據。另外,其他代碼可能構造一個對象,并傳入損壞的數據。因此,建議將如下的attribute應用于GetObjectData方法和特殊構造器:
[SecurityPermissionAttribute(SecurityAction.Demand,SerializationFormatter = true)]
格式化器序列化一個對象時,會檢查每個對象。如果發現一個對象的類型實現了ISerializable接口,格式化器就會忽略所有定制attribute,改為構造一個新的SerializationInfo對象,這個對象包含了要實際為對象序列化的值的集合。
構造一個SerializationInfo時,格式化器要兩個參數:Type和IFormatterConverter。Type參數標識要序列化的對象。為了唯一性地標識一個類型,需要兩個部分的信息:類型的字符串名稱及其程序集的標識。一個SerializationInfo對象構造好之后,會包含類型的全名(即Type的FullName),并將這個字符串存儲到一個私有字段中。為了獲取類型的全名,可使用SerializationInfo的FullTypeName屬性。通過調用SerializationInfo的SetType方法,傳遞目標Type對象的引用,用于設置FullTypeName和AssemblyName屬性。
構造好并初始化SerializationInfo對象后,格式化器調用類型的GetObjectData方法,傳遞SeriializationInfo對象。GetObjectData方法負責決定需要序列化的信息,然后將這些信息添加到SerializationInfo中。GetObjectData調用SerializationInfo類型的AddValue方法來指定要序列化的信息。需要對每個要添加的數據,都進行AddValue方法的調用。
下面代碼展示了Dictionary<TKey,TValue>類型如何實現ISerializable和IDeserializationCallback接口來控制其對象的序列化和反序列化工作。
四、在基類沒有實現ISerializable的情況下定義一個實現它的類型
之前提到,如果基類實現了ISerializable接口,那么它的派生類也必須實現ISerializable接口,同時還要調用基類的GetObjectData方法和特殊構造器。(見上文紅色字體)
但是,你可能要定義一個類型來控制它的序列化,但它的基類沒有實現ISerializable接口。在這種情況下,派生類必須手動序列化基類的字段,具體的做法是獲取它們的值,并把這些值添加到SerializationInfo集合中。然后,在特殊構造器中,還必須從集合中取出值,并以某種方式設置基類的字段。如果基類的字段是public或者protected字段,還容易實現。但,如果基類的private字段,那么則很難實現。
以下代碼實現如何正確實現ISerializable的GetObjectData方法和特殊的構造器:
[Serializable]
class Base
{
protected string name = "Mario";
public Base()
{
}
}
[Serializable]
class Derived : Base, ISerializable
{
private DateTime _date = DateTime.Now;
public Derived() { }
//如果這個構造器不存在,則會引發一個SerializationException異常
//如果此類不是密封類,這個構造器就應該是protected的
[SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
private Derived(SerializationInfo info, StreamingContext context)
{
Type baseType = this.GetType().BaseType;
MemberInfo[] memberInfos = FormatterServices.GetSerializableMembers(baseType, context);
for (int i = 0; i < memberInfos.Length; i++)
{
FieldInfo fieldInfo = (FieldInfo)memberInfos[i];
fieldInfo.SetValue(this, info.GetValue(baseType.FullName + "+" + fieldInfo.Name, fieldInfo.FieldType));
}
_date = info.GetDateTime("Date");
}
[SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
public virtual void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.AddValue("Data", _date);
Type baseType = this.GetType().BaseType;
MemberInfo[] memberInfos = FormatterServices.GetSerializableMembers(baseType,context);
for (int i = 0; i < memberInfos.Length; i++)
{
info.AddValue(baseType.FullName + "+" + memberInfos[i].Name, ((FieldInfo)memberInfos[i]).GetValue(this));
}
}
public override string ToString()
{
return string.Format("Name={0},Date={}", name, _date);
}
}
在代碼中,有一個名為Base的基類,它只用Serializable特性標識。其派生類Derived類,也使用了Serializable特性,同時還實現了ISerializable接口。同時兩個類還定義了自己的字段,調用SerializationInfo的AddValue方法進行序列化和反序列化。
解釋:
序列化: 每個AddValue方法都獲取一個String名稱和一些數據。數據一般是簡單的類型,當然我們也可以傳遞object引用。GetObjectData添加好所有必要的序列化信息之后,會返回至格式化器。現在,格式化器獲取已經添加到SerializationInfo對象的所有值,并把它們都序列化到流中。同時,我們還向GetObjectData方法中傳遞了另外一個參數StreamingContext對象的實例。當然,大多數類型的GetObjectData方法都忽略了此參數,下文詳細說明。
反序列化:格式化器從流中提取一個對象時,會為新對象分配內存(通過FormatterService.GetUninitializedObject方法)。最初,此對象的所有字段都為0或者是null。然后,格式化器檢查類型是否實現了ISerializable接口。如果存在此接口,格式化器則會嘗試調用我們定義的特殊構造函數,它的參數和GetObjectData是一致的。
如果類是密封類,則建議將此特殊構造聲明為private,這樣就可以防止其他代碼調用它。如果不是密封類,則應該將這個特殊構造器聲明為protected,保證派生類可以調用它。切記,無論這個特殊構造器是如何聲明的,格式化器都可以調用它的。
構造器獲取對一個SerializationInfo對象的引用,在這個SerializationInfo對象中,包含了對象(要序列化的對象)序列化時添加的所有值。特殊構造器可調用GetBoolean,GetChar,GetByte,GetInt32和GetValue等任何一個方法,向他傳遞與序列化一個值所用的名稱對應的一個字符串。以上的每個方法返回的值再用于初始化新對象的各個字段。
反序列化一個對象的字段時,應調用和對象序列化時傳給AddValue方法的值得類型匹配的一個Get方法。也就是說,如果GetObjectData方法調用AddValue時傳遞的是一個Int32值,那么在反序列化對象的時候,也應該為同一個值調用GetInt32方法。如果值在流中的類型和你要獲取的類型不匹配,格式化器則會嘗試用IFormatterConverter對象將流中的值轉換為你指定的類型。
上文中提到,構造SerializationInfo對象時,需要傳遞Type和IFormatterConverter接口的對象(此時,它是重點,不要被Type勾引走)。由于格式化器負責構造SerializationInfo對象,所以要由它選擇它需要的IFormatterConverter。.Net的BinaryFormatter和SoapFormatter構造的就是一個FormatterConverter類型,.Net的格式化器沒有提供一個讓你可以選擇的IFormatterConverter的實現。
FormatterConverter類型調用System.Convert類的各種靜態方法在不同的類型之間進行轉換,比如講一個Int16轉換為Int32。然而,為了在其他任意類型之間轉換一個值,FormatterConverter需要調用Convert的ChangeType方法將序列化好的類型轉換為一個IConvertible接口,然后再調用恰當的接口的方法。所以,要允許一個可序列化類型的對象反序列化成一個不同的類型,可以考慮讓自己的類型實現IConvertible接口。切記,只有在反序列化對象時調用Get方法,并且發現了類型和流中的值得類型不匹配時候,才會使用FormatterConverter對象。
特殊構造器也可以不調用上面的各種Get方法,而是調用GetEnumerator。此方法會返回一個SerializationInfoEnumerator對象,可使用該對象遍歷SerializationInfo對象中包含的所有的值。枚舉的每個值都是一個SerializationEntry對象。
當然,我們完全可以自定義一個類型,讓它實現ISerializable的GetObjectData方法和特殊構造器一個類型派生。如果我們的類型實現了ISerializable,那么可以在我們實現的GetObjectData方法和特殊構造器中,必須調用基類中的同名方法,以確保對象正確序列化和反序列化。這一點是必須的哦,否則對象時不能正確序列化和反序列化。
如果我們的派生類型中沒有其他的額外字段,當然也沒有特殊的序列化和反序列化需求,就不用事先ISerializable接口。和其他接口成員相似,GetObjectData是virtual的,調用它可以正確的序列化對象。格式化器將特殊構造器視為“已虛擬化”,也就是說,反序列化過程中,格式化器會檢查要實例的類型,如果那個類型沒有提供特殊的特殊構造器,則會看其基類是否存在,知道找到一個實現了特殊構造器的一個類。
注意:特殊構造器中的代碼一般會從傳給 它的SerializationInfo對象中提取字段。提取了字段后,不能保證對象已完全反序列化,所以特殊構造器中的代碼不應嘗試操縱它提取的對象。如果我們的類型必須訪問提取的一個對象中的成員,最好我們的類型提供一個應用了OnDeserialized特性的方法,或者讓我們的類型實現IDeserializationCallback接口的OnDeserialization方法。調用該方法時,所有對象的字段都已經設置好。然而,對于多個對象來說,它們的OnDeserialized或OnDeserialization方法的調用順序是沒有保障的。所以,雖然字段可能已經初始化,但我們仍然不知道被引用的對象是否已完全反序列化好(如果那個被引用的對象也提供了一個OnDeserialized方法或者實現了IDeserializationCallback)。
P.S. 必須調用AddValue方法的某個重載版本為自己的類型添加序列化信息。如果一個字段的類型實現了ISerializable接口,就不要在字段上調用GetObjectData,而應該調用AddValue來添加字段。格式化器會發現字段的類型實現了ISerializable,會自動調用GetObjectData。如果自己在字段上調用了GetObjectData,格式化器則不會知道在對流進行反序列化時創建一個新對象。
五、將類型序列化為不同的類型以及將對象反序列化為不同的對象
[Serializable]
public class Student : ISerializable
{
private string _name;
public string Name
{
get { return _name; }
set { _name = value; }
}
[SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.SetType(typeof(SerializationHelper));
}
}
[Serializable]
public class SerializationHelper : IObjectReference
{
public object GetRealObject(StreamingContext context)
{
return "新的類型哦";
}
}
上述代碼中一個我們的數據類Student,還有一個序列化幫助類,其中Student類就是我們要序列化的類,幫助類就是為了告訴代碼我們要把Student類序列化為它,并且再反序列化的時候也應該是它。
測試下:
static void Main(string[] args)
{
Student student = new Student { Name = "馬里奧" };
using (var stream = new MemoryStream())
{
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(stream, student);
stream.Position = 0;
var deserializeValue = formatter.Deserialize(stream);
Console.Write(deserializeValue.ToString());
Console.Read();
}
}
可以看到結果:
P.S.ISerializable:允許對象控制其自己的序列化和反序列化過程。
IObjectReference:指示當前接口實施者是對另一個對象的引用。
好了,序列化和反序列化的東西說的也差不多了,大家有什么更好的想法可以和我交流。
總結
以上是生活随笔為你收集整理的C#之你懂得的序列化/反序列化的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 光学像差理论
- 下一篇: 农行乐分易手续费多少?现在办理享5折优惠