C#基础知识梳理系列十四:序列化
说到序列化,大家都非常熟悉XML序列化,还有二进制序列化,经过序列化的数据流更方便传输和存储。其实我们可以对序列化进行更多的控制,比如对序列化(和反序列化)前后的数据操作、定义自己的可序列化类型等。这一章我们来讨论一下有关于序列化和反序列化。
1、 序列化
序列化包括正向序列化和反向序列化,一般我们将正向序列化说成是序列化。
序列化(Serialization)是将一个类对象转化成一个字节流。
反序列化(Deserialization)是将一个字节流转化成一个对应的类对象的过程。
在WCF通信中,当向服务端发送请求的时候,WCF是先把本地的内存对象序列化成XML或Binary通过信道传送给服务端,服务端是把接收到序列化后的数据再将其转化成对应的类对象,当服务端处理完毕向客户发送数据时是一个恰恰相反的序列化/反序列化过程。
经过序列化后的流数据可以持久化,也可以跨平台传送,比如WebService。
2、 C#提供的XML和二进制序列化
C#提供了两种主要的序列化:XmlSerializer和BinaryFormatter,SoapFormatter已经不再使用。
XmlSerializer是XML序列化,是将对象序列化成XML及其反向的过程。
如下代码是将一个Student对象序列化到一个XML文件:
private void TestXmlSerializer() { Student student = new Student() { Name = "小明", Age = 15 }; XmlSerializer xs = new XmlSerializer(typeof(Student)); using (Stream stream = new FileStream("c:\\Student.xml", FileMode.Create, FileAccess.Write, FileShare.Read)) { xs.Serialize(stream, student); } }
最终的XML文档:
<?xml version="1.0"?> <Student xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <Name>小明</Name> <Age>15</Age> </Student>
我们当然也可以从一个XML文档反序列化到一个对象:
using (FileStream fs = new FileStream("c:\\Student.xml", FileMode.Open, FileAccess.Read)) { student = (Student)xs.Deserialize(fs); }
再来看一下BinaryFormatter:
private void TestBinaryFormatter() { // Student student = new Student() { Name = "小明", Age = 15 }; //序列化 using (FileStream fileStream = new FileStream("c:\\Student.dat", FileMode.Create)) { BinaryFormatter b = new BinaryFormatter(); b.Serialize(fileStream, student); } //反序列化 using (FileStream fileStream = new FileStream("c:\\Student.dat", FileMode.Open,FileAccess.Read)) { BinaryFormatter bf = new BinaryFormatter(); student = (Student)bf.Deserialize(fileStream); } }
无论是XML序列化还是二进制序列化,从以上示例可以看到,都是以流作为媒介,在得到一个对象的数据流后,我们可以对流进行传送、加密等操作。
有一点要注意的是,不能使用XmlSerializer反序列化BinaryFormatter序列化过的流,也不能用BinaryFormatter反序列化XmlSerializer序列化过的流,虽然二者都是序列格式化器,但二者不能通用。
BinaryFormatter还支持将多个类型的对象序列化到一个流中,如下:
private void TestBinaryFormatter() { // Teacher teacher = new Teacher() { Name = "王老师", Age = 45 }; Student student = new Student() { Name = "小明", Age = 15 }; //序列化 using (FileStream fileStream = new FileStream("c:\\Student.dat", FileMode.Create)) { BinaryFormatter b = new BinaryFormatter(); b.Serialize(fileStream, teacher); b.Serialize(fileStream, student); } //反序列化 using (FileStream fileStream = new FileStream("c:\\Student.dat", FileMode.Open, FileAccess.Read)) { BinaryFormatter bf = new BinaryFormatter(); teacher = (Teacher)bf.Deserialize(fileStream); student = (Student)bf.Deserialize(fileStream); } }
但是XmlSerializer不支持类似上面的操作,因为在构造XmlSerializer格式化器的时候已经明确了即将进行序列化的类型,如果想实现类似BinaryFormatter的操作,可以将多个对象包装进一个对象中,然后再进行序列化,如下的一个教师学生类中包括了Teacher和Student:
[Serializable] public class TeacherStudent { public Teacher Teacher { get; set; } public Student Student { get; set; } } //看一下序列化/反序列化代码: TeacherStudent ts = new TeacherStudent(); ts.Teacher = new Teacher() { Name = "王老师", Age = 45 }; ts.Student = new Student() { Name = "小明", Age = 15 }; XmlSerializer xsts = new XmlSerializer(typeof(TeacherStudent)); using (Stream stream = new FileStream("c:\\TeacherStudent.xml", FileMode.Create, FileAccess.Write, FileShare.Read)) { xsts.Serialize(stream, ts); } using (FileStream fs = new FileStream("c:\\TeacherStudent.xml", FileMode.Open, FileAccess.Read)) { ts = (TeacherStudent)xsts.Deserialize(fs); }
序列化/反序列化是基于反射实现的。
序列化过程中,格式化器首先调用类FormatterServices的一个静态方法GetSerialization获取所有公共和私有字段成员MemberInfo(未标记NonSerialized)数组,然后根据这个成员数组获取其对应的值数组,接着向流中写入程序集及类型信息,最后是遍历以上两个数组将对应的值写入流中。
反序列化是序列化的一个相逆过程,格式化器先从流中读取程序集及类型信息,然后加载相应的程序集,接着初始化对应的类型对象,再是构造该对象的成员数组,然后从流中获取相应的值数组,最后是将从流中获取的值数组与类型对象的成员数组匹配并为对象成员赋值。
在序列化一个对象的时候,可能想明确要求某个类型可被序列化,也可能不希望某些字段被序列化,我们可以附加类型或字段的特性System. SerializableAttribute来达到要求。
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Delegate, Inherited = false)] public sealed class SerializableAttribute : Attribute { public SerializableAttribute(); }
如上面的类TeacherStudent前就使用了特性[Serializable]。SerializableAttribute特性只能应用于类型、值类型、枚举和委托类型,该特性是不能被继承的,如下两个类型:
[Serializable] public class A { } public class B : A { }
类B是获取不到类A的可序列化特性的,如果要使类型B可序列化,必须对其使用特性Serializable。
对于XmlSerializer序列化,默认即使不使用特性Serializable,也是可以对对象进行序列化的,则BinaryFormatter不然。在创建WCF应用时,WCF强制要求所有传输的对象必须使用数据契约,这个数据契约是WCF特有的一种面向服务的格式化器:DataContract和DataMember,有关这一部分,可参考MSDN文档。
特性Serializable用于类型时,该类型的所有字段都会被序列化,当我们希望某个字段不可被序列化时,可以使用特性System. NonSerializedAttribute:
[AttributeUsage(AttributeTargets.Field, Inherited = false)] public sealed class NonSerializedAttribute : Attribute { public NonSerializedAttribute(); } //它只能用于字段。如下代码: [Serializable] public class TeacherStudent { public Teacher Teacher { get; set; } public Student Student { get; set; } [NonSerialized] private int RootID; }
则字段RootID就不会被序列化。
另外,有时候由于版本或是远程服务调用的问题,可能会出现类型成员与序列化后的数据流不一致的现象,比如类型中的字段比序列化后的数据流多了一个,这时如果强制反序列化,可能会出异常,因为在流数据中未能找到与对象对应的某一字段,为了解决这个问题,可以将新添加的字段附加特性OptionalField。如下我们为Student类型增加一个年级的字段:
[OptionalField] private int Grade;
有时候,我们想对序列化前后的数据进行更多的控制,比如在序列化前对对象附加数据,把序列化后的流加密,反序列化之后对对象的数据进行修正等等,这些都是可以做到的。在可序列化类型中定义相应的方法并对方法使用特性,然后在序列化/反序列化时,格式化器会检查类型中是否有相应的方法定义,如果有,则调用此方法。对于序列化/反序列化,通常有以下四个相应特性及方法:
(1) OnSerializing 序列化之前
[OnSerializing] private void OnSerializing(StreamingContext context) { } //格式化器在序列化开始之前调用此方法。
(2) OnSerialized 序列化之后
[OnSerialized] private void OnSerialized(StreamingContext context) { } //格式化器在序列化后调用此方法。
(3) OnDeserializing 反序列化之前
[OnDeserializing] private void OnDeserializing(StreamingContext context) { } //格式化器在反序列化开始之前调用此方法。
(4) OnDeserialized 反序列化之后
[OnDeserialized] private void OnDeserialized(StreamingContext context) { } //格式化器在反序列化开始之后调用此方法。
如下代码,我们对Student类型,在序列化前判断,如果年龄小于7周岁,则为它赋默认值为7,在反序列化之后也进行相应的操作(当然,这里可以使用属性的来达到相同的目的,但我们这里只是演示如何控制序列化和反序列化前后的数据)。
[Serializable] public class Student { public string Name { get; set; } public int Age { get; set; } [OnSerializing] private void OnSerializing(StreamingContext context) { if (this.Age<7) { this.Age = 7; } } [OnSerialized] private void OnSerialized(StreamingContext context) { } [OnDeserializing] private void OnDeserializing(StreamingContext context) { } [OnDeserialized] private void OnDeserialized(StreamingContext context) { if (this.Age < 7) { this.Age = 7; } } }
注意:以上的控制方法只对BinaryFormatter和SoapFormatter有效,对XmlSerializer无效。四个方法名可以是任意的,只是必须接收参数StreamingContext。StreamingContext是一个流上下文,它有两个只读属性:
State 获取传输数据的源或目标。
Context 获取指定为附加上下文一部分的上下文信息。
通过前面的讨论,我们知道可以使用序列化/反序列化前后对应的四个方法来控制可序列化的数据,但是由于序列化是基于反射实现,它对性能有一定的损伤,并且这四个方法只能提供有限的操作。实现System.Runtime.Serialization. ISerializable接口的类型,不但可以相应提高性能,也提供了对序列化更丰富的操作。
public interface ISerializable { [SecurityCritical] void GetObjectData(SerializationInfo info, StreamingContext context); }
ISerializable接口只有一个方法,该方法负责决定使用哪些信息来序列化对象,并调用SerializationInfo的AddValue方法将这些信息添加到SerializationInfo对象中。在序列化过程中,格式化器如果发现类型实现了接口ISerializable,则格式化器会调用GetObjectData方法。
下面我们创建一个实现了ISerializable接口的类People:
[Serializable] public class People : ISerializable { string _name; public string Name { get { return _name; } set { _name = value; } } int _age; public int Age { get { return _age; } set { _age = value; } } public People() { } [SecurityCritical] public void GetObjectData(SerializationInfo info, StreamingContext context) { info.AddValue("Name", _name); info.AddValue("Age", _age); } }
我们再来看一下对反序列化支持。在反序列化的过程中,格式化器如果发现类型实现了ISerializable接口,则格式化器会尝试调用该类型的一个特殊的构造器,这个构造器的参数列表必须与GetObjectData的完全相同,通常是将该构造器声明为私有或是受保护的。在构造器内会从SerializationInfo参数中读取类型对应的字段数据(调用SerializationInfo的GetXXX方法)然后赋值给当前对象相应的字段。此处调用的GetXXX方法必须与其AddValue方法对应的数据类型一致,如People的构造器:
[SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)] protected People(SerializationInfo info, StreamingContext context) { _name = info.GetString("Name"); _age = info.GetInt32("Age"); }
添加string类型的Name:info.AddValue("Name", _name);
必须调用获取string类型的方法:_name = info.GetString("Name");
并且都是操作相同的属性(字段)名。
使用实现了接口ISerializable的类型与之前无差别,如下:
//序列化 using (FileStream fileStream = new FileStream(string.Format("c:\\Serializer1\\People{0}.dat", i), FileMode.Create)) { BinaryFormatter b = new BinaryFormatter(); b.Serialize(fileStream, people); } //反序列化 using (FileStream fileStream = new FileStream(string.Format("c:\\Serializer1\\People{0}.dat", i), FileMode.Open, FileAccess.Read)) { BinaryFormatter bf = new BinaryFormatter(); People p = null; p = (People)bf.Deserialize(fileStream); }
注意,如果一个类型继承了实现接口ISerializable的类型,那么在派生类的GetObjectData方法中必须先调用基类的方法:base.GetObjectData(info, context)。