五,格式化器如何序列化类型实例
为了简化格式化的操作,FCL在System.Runtime.Serialization命名空间提供了一个FormatterServices类型。该类型只包含静态方法,而且不能被实例化。以下的步骤描述了格式化器如何自动序列化一个应用了SerializableAttribute的对象。
1,格式化器调用FormatterServices的GetSerializableMembers方法:
public static MemberInfo[] GetSerializableMembers(Type type,StreamingContext context)
这个方法利用反射获取类型的public和private实例字段(标记了NonSerializedAttribute的字段除外)。方法返回一个MemberInfo对象的数组,其中每个元素都对应于一个可序列化的实例字段。
2,对象序列化,System.Reflection.MemberInfo对象数组传给FormatterServices的静态方法GetObjectData:
public static Object[] GetObjectData(Object obj,MemberInfo[] members)
这个方法返回一个object数组,其中每个元素都标识了被序列化的那个对象中的一个字段的值。这个object数组和MemberInfo数组是并行的;换言之,object数组的元素0是MemberInfo数组中的元素0所标识的那个成员的值。
3,格式化器将程序集标识和类型的完整名称写入流中。
4,格式化器然后遍历两个数组中的元素,将每个成员的名称写入流中。
以下的步骤描述了格式化器如何自动反序列化一个(其类型)应用了SerializableAttribute的对象。
1,格式化器从流中读取程序集标识和完整类型名称。
如果程序集没有加载到单前的AppDomain中,加载它。如果程序集不能加载,就抛出SerializationException异常,对象不能序列化。如果程序集已加载,格式化器将程序集信息和类型全名传给FormatterServices的静态方法GetTypeFromAssembly:
public static Type GetTypeFromAssembly(Assembly assem, string name)
这个方法返回一个System.Type对象,它代表要反序列化的那个对象的类型。
2,格式化器调用FormatterServices的静态方法GetUninitializedObject:
public static Object GetUninitializedObject(Type type)
这个方法为一个新对象分配内存,并不为对象调用构造器。然后,对象的都有字节都被初始化为null或0。
3,格式化器现在构造并初始化一个MemberInfo数组。
具体做法和前面的做法一样,都是调用FormatterServices的GetSerializableMembers方法。这个方法返回序列化好,现在需要反序列化的一组字段。
4,格式化器根据流中包含的数据创建并初始化一个object的数组。
5,将新分配的对象、MemberInfo数组以及并行object数组(其中包含字段值)的引用传给FormatterServices的静态方法PopulateObjectMembers:
public static Object PopulateObjectMembers(Object obj,MemberInfo[] members, Object[] data)
这个方法遍历数组,将每个字段初始化成对应的值。到此为止,对象就算是被彻底反序列化了。
六,控制序列化/反序列化的数据
前面讨论过,控制序列化和反序列化的最佳方式就是使用OnDeserializedAttribute, OnDeserializingAttribute,OnSerializedAttribute,OnSerializingdAttribute,NonSerializedAttribute和OptionalFieldAttribute等特性。然而,在一些极少见的情况下,这些attribute不能提供你希望的全部控制。除此之外,格式化器在内部使用反射,而反射的速度比较慢,这会增大序列化和反序列化的时间。为了对序列化和反序列化数据进行完全的控制,并避免使用反射,你的类型可以实现System.Runtime.Serialization.ISerializable接口,它的定义如下:
public interface ISerializable { void GetObjectData(SerializationInfo info, StreamingContext context); }
这个接口只有一个方法,即GetObjectData。但是实现这个接口的大多数类型还实现了一个特殊的构造器。
重要提示:ISerializable接口的一个大问题在于,一旦类型实现了它,所有的派生类型也必须实现它,而且派生类型必须保证调用基类的GetObjectData方法和特殊构造器。除此之外,一旦类型实现了该接口,便永远不能删除它,否则会失去与派生类型的兼容性。
重要提示:ISerializable接口和特殊的构造器旨在有格式化器使用。然而其他一些代码可能调用GetObjectData,后者可能返回敏感的数据。另外,其他代码可能构造一个对象,并传入损坏的数据。因此,建议将以下attribute应用于GetObjectData方法和特殊构造器:
[SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)]
格式化器序列化一个对象时,会检查每个对象。如果发现一个对象的类型实现了ISerializable接口,格式化器就会忽略所有定制attribute,改为构造一个System.Runtime.Serialization.SerializationInfo对象。这个对象包含了要实际为对象序列化的值的集合。
构造一个SerializationInfo时,格式化器要传递两个参数:Type和System.Runtime.Serialization.IFormatterConverter。Type标识要序列化的对象。为了唯一的标识一个类型,需要两个部分的信息:类型的字符串名称及其程序集的标识。一个SerializationInfo对象构造好后,会包含类型的全名,并将这个字符串存储在一个私有字段中。可以通过SerializationInfo的FullTypeName属性查询。类似的,构造器获取类型的定义程序集(通过在内部查询Type的Module属性,再查询Module的Assembly属性,在查询Assembly的FullName属性),并将这个字符串存储在一个私有字段中。可以通过SerializationInfo的AssemblyName属性查询。虽然可以设置这两个属性,但不建议这么做。如果想更改序列化的类型,可以调用SerializationInfo的SetType方法,调用这个方法后,这两个属性会被正确的设置。构造好并初始化好SerializationInfo对象后,格式化器调用类型的GetObjectData方法,向它传递对SerializationInfo对象的引用。GetObjectData方法负责决定需要哪些信息来序列化对象,并将这些信息添加到SerializationInfo对象中。GetObjectData调用SerializationInfo提供的AddValue方法的众多重载版本之一来指定要序列化的信息,针对每个要添加的数据,都要调用一次AddValue方法。
以下的代码展示了自定义的MyType类型如何实现ISerializable和IDeserializationCallback接口来控制其对象的序列化和反序列化工作。
[Serializable] public class MyType : ISerializable, IDeserializationCallback { Int32 x, y; Int32 sum; SerializationInfo m_siInfo; public MyType(int x, int y) { this.x = x; this.y = y; this.sum = x + y; } //用于控制反序列化的特殊构造器,参数列表必须和GetObjectData一致 //这个构造器会在OnDeserialization之前调用 protected MyType(SerializationInfo info, StreamingContext context) { m_siInfo = info; } #region ISerializable //用于控制序列化的方法 public void GetObjectData(SerializationInfo info, StreamingContext context) { //这里只序列化了x和y,并没有sum info.AddValue("left", x); //注意这里的名称可以任意取,不一定是字段名 info.AddValue("right", y); } #endregion #region IDeserializationCallback //所有key/value对象都反序列化好后调用的方法 public void OnDeserialization(object sender) { if (m_siInfo == null) return; this.x = m_siInfo.GetInt32("left");//读取的时候,名称要和GetObjectData指定的一致 this.y = m_siInfo.GetInt32("right"); this.sum = x + y; } #endregion }
●每个AddValue方法都获取一个string名称和一些数据。数据一般是简单类型,如Int32,Boolean,DateTime等。然而也可以传递一个object的引用。GetObjectData添加好所有必要的序列化信息之后,会返回值格式化器。注意,如果一个字段类型实现了ISerializable接口,不要调用GetObjectData方法,相反直接调用AddValue添加字段。格式化器会帮你调用GetObjectData。现在,格式化器获取已经添加到SerializationInfo对象的所有值,并把它们序列化到流中。
●知道了如何设置序列化所需的全部信息之后,再来看反序列化。格式化器从流中提取一个对象时,会为对象分配内存(调用FormatterServices的静态方法GetUninitializedObject)。最初,这个对象的所有字段都设置成null或0。然后格式化器查看类型是否实现了一个ISerializable接口。如果存在这个接口,格式化器尝试调用一个特殊构造器,它的参数和GetObjectData方法完全一致。如果你的类型是sealed,建议将这个构造器声明为private,防止任何代码不慎调用它。注意,无论这个构造器是如何声明的,格式化器都能调用它。
●构造器获取一个对SerializationInfo对象的引用。在这个SerializationInfo对象中,包含了对象序列化时添加的所有值。特殊构造器可以调用GetInt32,GetBoolean等方法获取设定的值。反序列化时,调用的GetXXX方法一定要和GetObjectValue方法调用的AddValue时传递的字段类型一致。它们是一一匹配的,如果类型不一致,格式化器会尝试用一个IFormatterConverter对象将流中的值“转型”成你指定的值。
●构造SerializationInfo时,需要传递一个实现IFormatterConverter接口的对象。由于SerializationInfo的创建是由格式化器负责的,所有由它来选择想要的IFormatterConverter类型。Microsoft的BinaryFormatter和SoapFormatter类型都是构造System.Runtime.Serialization.FormatterConverter的一个实例。Microsoft的格式化器没有提供任何方式让我们自己选择IFormatterConverter类型。
●FormatterConverter类型调用System.Convert类的各种静态方法在不同的类型之间进行转化。然而,为了在其他任意类型之间进行转换,FormatterConverter类型要调用Convert的ChangeType方法将序列化好的类型转化成一个IConvertible接口,在调用恰当的方法。所有,要允许一个可序列化类型的对象反序列化成一个不同的类型,可以考虑自己实现IConvertible接口。注意,在反序列化时调用一个Get方法,只有发现它的类型和流中的类型不一致时,才会使用FormatterConverter对象。
●特殊构造器也可以不调用Get方法,而是调用GetEnumerator,返回一个System.Runtime.Serialization.SerializationInfoEnumerator。该对象可以遍历SerializationInfo的所有值。枚举的每个值都是System.Runtime.Serialization.SerializationEntry对象。
● 当然,你完全可以定义一个自己的类型,让它从实现了ISerializable的GetObjectData方法和特殊构造器的一个类型派生。如果你的类型也实现了ISerializable,那么在你实现的GetObjectData方法和特殊构造器中,必须调用基类的同名方法,确保对象能序列化和反序列化,这一点必须牢记 。如果派生类中没有额外的字段,因而没有特殊的序列化/反序列化的需求,就完全不必实现ISerializable。和所有的接口成员相似,GetObjectData是virtual的,调用它可以正确的序列化对象。除此之外,格式化器将特殊构造器视为“已虚拟化”(virtualized)。换言之,反序列化期间,格式化器会检查要序列化的类型。如果那个类型没有提供特殊构造器,格式化器会扫描基类,直到找到实现特殊构造器的类。
重要提示:特殊构造器中的代码一般会从传给它的SerializationInfo对象中提取字段。提取字段后,不能保证对象已完全序列化,所以,特殊构造器不应尝试操纵它的对象。如果你的类型必须访问提取的一个对象中的成员(比如调用一个方法),建议你的类型提供一个应用了OnDeserialized特性的方法,或者让你的类型实现IDeserializationCallback接口的OnDeserialization方法。调用该方法时,所有的字段都已设置好。然而对于多个对象来说,它们的OnDeserialized或OnDeserialization的调用顺序是没有保障的。所以,虽然字段可能已经初始化,但你仍然不知道被引用的对象是否已完全反序列化好(如果那个被引用的对象也实现了OnDeserialized或IDeserializationCallback)。
如何在基类没有实现ISerializable接口的前提下定义一个实现它的类型
前面已经讲过,ISerializable的功能非常强大,它允许一个类型完全控制如何对类型的实例进行序列化和反序列化。然而,这个能力是有代价的,子类必须负责基类所有字段的序列化。如果基类实现了ISerializable接口,子类只需调用基类的GetObjectData方法即可。
但有一天,我们需要对象子类进行序列化,但发现它的基类没有实现ISerializable接口。在这种情况下,派生类必须手动序列化基类的字段,具体做法就是获取他们的值,并把这些值添加到SerializationInfo集合中。然后在特殊构造器中,还必须从集合中取出这些值,并以某种方式设置基类的字段。如果基类的字段是public或protected字段,那么这一切都容易实现。但如果基类的字段是private字段,就很难或更本不可能实现。下面的例子演示了如何正确实现ISerializable的GetObjectData和他的特殊构造器使基类的字段能被序列化:
[Serializable] internal class Base { protected string m_name; public Base() { } public Base(string name) { m_name = name; } } [Serializable] internal class Derived : Base, ISerializable { private DateTime m_date; public Derived(string name) : base(name) { m_date = DateTime.Now; } //如果这个特殊构造器不存在,会抛出SerializationException异常 [SecurityPermission(SecurityAction.Demand, SerializationFormatter = false)] private Derived(SerializationInfo info, StreamingContext context) { //为本类设置反序列化好的值 m_date = info.GetDateTime("date"); //查找基类的可序列化字段集合 Type baseType = this.GetType().BaseType; MemberInfo[] members = FormatterServices.GetSerializableMembers(baseType, context); foreach (MemberInfo mi in members) { //为基类设置反序列化好的值 FieldInfo fi = (FieldInfo)mi; fi.SetValue(this, info.GetValue(baseType.FullName + "+" + mi.Name, fi.FieldType)); } } #region ISerializable public void GetObjectData(SerializationInfo info, StreamingContext context) { //为本类序列化值 info.AddValue("date", m_date); //查找基类的可序列化字段集合 Type baseType = this.GetType().BaseType; MemberInfo[] members = FormatterServices.GetSerializableMembers(typeof(Derived).BaseType, context); foreach (MemberInfo mi in members) { //为基类设置序列化值,最好加上基类的前缀名,避免和子类有同名成员时冲突 info.AddValue(baseType.FullName + "+" + mi.Name, ((FieldInfo)mi).GetValue(this)); } } #endregion }
特别注意:经实验发现,调用特殊化构造器的时候,会去调用基类的默认构造函数,感觉这个设计似乎有些问题。
未完待续,下接《CLR via C#》笔记——运行时序列化(3)