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:指示当前接口实施者是对另一个对象的引用。
好了,序列化和反序列化的东西说的也差不多了,大家有什么更好的想法可以和我交流。