前言:写此文章一方面是为了巩固对序列化的认识,另外一方面是由于本人最近在面试,面试中被问到“为何要序列化”。虽然一直在使用,本身也反复的提到序列化,可至于说为何要序列化,还真的没想过,因此本文就这样产生了。面试
序列化是将一个对象转换成一个字节流的过程。反序列化是将一个字节流转换回对象的过程。在对象和字节流之间转换是颇有用的一个机制。(固然这个还不能回答它的实际用处)数据库
举点例子:数组
除了上述的几个场景,咱们能够将系列化获得的字节流进行任意的操做。网络
1、序列化、反序列化快速实践ide
[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会很智能的序列化每一个对象都只序列化一次,防止进入无限循环。this
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);
2、控制序列化和反序列化
若是给类添加了SerializeAttribute,那么类的全部实例字段(private、protected、public等)都会被序列化。可是,有时候类型中定义了一些不该序列化的实例字段。
通常状况下,如下两种状况不但愿序列化字段:
使用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应用于一个字段时,就不会由于流中的数据不包含这个字段而出现异常。
3、序列化和反序列化的原理
为了简化格式化器的操做,在System.Runteime.Serialization中有一个FormatterServices类型。该类型只包含静态方法,而且该类为静态类。
Serialize步骤:
public static MemberInfo[] GetSerializableMembers(Type type,StreamContext context);
这个方法利用反射获取类型的public和private实例字段(除了标识为NonSerializedAttribute的字段除外)。方法返回由MemberInfo对象构成的一个数组,其中每一个元素都对应于一个可序列化的实例字段。
public static object[] GetObjectData(Object obj,MemberInfo[] members);
这个方法返回一个Object数组,其中每一个元素都标识了被序列化的那个对象的一个字段的值。这个Object数组和MemberInfo数组是并行的;也就是说,Object数组中的元素0是MemberInfo数组中的元素0所标识的那个成员的值。
Deserialize步骤:
public static Type GetTypeFromAssembly(Assembly assembly, string name);
这个方法返回一个Type对象,表明要反序列化的那个对象的类型。
public static Object GetUninitializedObject(Type type);
这个方法为一个新对象分配内存,并不为对象调用构造函数。因此,对象的全部字段都被初始化为null或者0;
public static Object PopulateObjectMembers(Object obj,MemberInfo[] members, Object [] data);
这个方法遍历数组,将每一个字段初始化成对应的值。到这里,就算反序列化结束了。
4、控制序列化/反序列化的数据
本文上述,有提到如何使用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接口来控制其对象的序列化和反序列化工做。
4、在基类没有实现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,格式化器则不会知道在对流进行反序列化时建立一个新对象。
5、将类型序列化为不一样的类型以及将对象反序列化为不一样的对象
[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:指示当前接口实施者是对另外一个对象的引用。
好了,序列化和反序列化的东西说的也差很少了,你们有什么更好的想法能够和我交流。