本章前面讨论过,控制序列化和反序列化过程的最佳方式就是使用OnSerializing、OnSerialized、OnDeserializing、OnDeserialized、NonSerialized、OptionalField等attribute。然而,在一些极少见的情况下,这些attribute不能提供你希望的全部控制。除此之外,格式化器在内部使用了反射,而反射的速度比较慢,这会增大序列化和反序列化对象所化的时间。为了序列化/反序列化的数据进行完全的控制,并避免使用反射,你的类型可实现ISerializable接口:
public interface ISerializable
{
void GetObjectData(SerializationInfo info, StreamingContext context);
}
这个接口只有一个方法,即GetObjectData,但是,实现这个接口的大多数类型还实现了一个特殊的构造器,稍后会详细描述它。
重要提示:ISerializable接口大问题在于,一旦类型实现了它,所有派生类型都必须实现他,而且派生类型必须能保证调用基类的GetObjectData方法和特殊构造器。除此之外,一旦类型实现了该接口,变永远不能删除它,否则会失去与派生类型的兼容性。Sealed类型实现了ISerializable接口总是可行的。
重要提示:ISerializable接口和特殊的构造器旨在由格式化器使用。然而,其他代码可能调用GetObjectData,后者可能返回敏感的数据。另外,其他代码可能构造一个对象,并传入损坏的数据,因此,建议将以下attribute应用于GetObjectData方法和特殊指令
[SecurityPermissionAttribute(SecurityAction.Demand,SerializationFormatter=true]
格式化器序列化一个对象图时,会检查每个对象。如果发现一个对象的类型实现了ISerializable接口,格式化器就会忽略所有定制的attribute,该为构造一个新的SerializationInfo对象,这个 对象包含了要实际为对象序列化的值的集合。
构造一个SerializationInfo对象时,格式化器要传递两个参数:Type和IFormatterConverter。Type参数标识了要序列化的对象。为了唯一的标识一个类型,需要两部分信息:类型的字符串名称以及程序集的标识(包括程序集名称、版本、语言文化和公钥)。一个SerializationInfo对象构造好之后,会包含类型的全名(通过查询Type的FullName属性),并将这个额字符串存储到一个私有字段中。为了获取类型的全名可查询SerializationInfo的FullTypeName属性。类似的,构造器获取类型的定义程序集,并将这个字符串存储到一个私有字段中。为了获取这个程序集标识,可查询SerializationInfo的AssemblyName属性。
构造好并初始化好SerializationInfo对象后,格式化器调用类型的GetObjectData方法,向它传递对SerializationInfo对象的引用。GetObjectData方法负责决定需要哪些信息来序列化对象,并将这些信息添加到SerializationInfo对象中。GetObjectData调用SerializationInfo类型提供的AddValue方法的众多重载版本之一来指定要序列化的信息。针对添加的每个数据,都要调用一次AddValue。
以下代码展示了Dictionary<TKey,Tvalue>类型如何实现ISerializable和IDeserializationCallback接口控制序列化和反序列化:
[Serializable] public class Dictionary<TKey, TValue> : ISerializable, IDeserializationCallback { private SerializationInfo m_siInfo;//只用于反射 //用于控制反序列化的特殊构造器 [SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)] protected Dictionary(SerializationInfo info, StreamingContext context) { //反序列化期间,为OnDeserialization保存SerializationInfo m_siInfo = info; } //用于控制序列化的方法 [SecurityCritical] public virtual void GetObjectData(SerializationInfo info, StreamingContext context) { info.AddValue("Version", m_version); info.AddValue("Comparer", m_comparer, typeof(IEqualityComparer<TKey>)); info.AddValue("HashSize", (m_buckets == null) ? 0 : m_buckets.Length); if (m_buckets != null) { KeyValuePair<TKey, TValue> array = new KeyValuePair<TKey, TValue>(count); CopyTo(array, 0); info.AddValue("KeyValuePairs", array, typeof(KeyValuePair<TKey, TValue>[])); } } //所有key/value对象都反序列化好之后调用的方法 public virtual void IDeserializationCallback.OnDeserialization(Object sender) { if (m_siInfo == null) return; Int32 num = m_siInfo.GetInt32("Version"); Int32 num2 = m_siInfo.GetInt32("HashSize"); m_siInfo.GetValue("Comparer", typeof(IEqualityComparer<TKey>)); if (num2 != 0) { m_buckets = new Int32[num2]; for (Int32 i = 0; i < m_buckets.Length; i++) m_buckets[i] = -1; m_entries=new Entry<TKey,TValue>[num2]; m_freeList = -1; KeyValuePair<TKey, TValue>[] pairArray =(KeyValuePair<TKey, TValue>[]); m_siInfo.GetValue("KeyValuePairs", typeof(KeyValuePair<TKey, TValue> [])); 。。。。。。。。 } }
每个AddValue方法都回去一个String名称和一些数据。数据一般是简单的值类型,比如Boolen,char,byte,sbyte,int16,uint32,int64,uint64,single,double,decimal,datetime.然而,还可以在调用AddValue时向它传递一个Object的引用。GetObjectData添加好所有的序列化信息之后,会返回至格式化器。
现在,格式化器获取已经添加到SerializationInfo对象的所有值,并把他们序列化到流中。注意,我们还向GetObjectData方法传递了另一个参数,也就是对一个StreamingContext对象的引用。大多数的GetObjectData都会忽略这个参数。后面讲讨论它。
知道了如何设置序列化所需的信息之后,再来看看反序列化。格式化器从流中读取一个对象时,会为新对象分配内存(FormatterServices.GetUninitializedObject)。最初,这个对象的所有字段都设为0或null。然后,格式化器检查类型是否实现了ISerializable接口。如果存在这个接口,格式化器就尝试调用一个特殊构造器,它的参数和GetObjectData方法完全一致。
如果你的类是密封类,强烈建议将这个特殊构造器声明为private。这样可防止任何代码不慎调用它,从而提升安全性。如果不是密封类,应该将这个特殊构造器声明为protect,确保之后派生类才能调用它。注意,无论这个特殊的构造函数时如何声明的,格式化器都能调用它。
构造器获取对一个SerializationInfo对象引用。在这个SerializationInfo对象中,包含了对象序列化时添加的所有值。特殊构造器可调用GetBoolean,GetChar,GetBtye,GetSByte,GetUInt16,GetInt32,GetUInt32,GetInt64,GetUInt64,GetSingle,GetDouble,GetDecimal,GetDateTime,GetString和GetValue等任何一个方法,向它传递与序列化一个值所用的名称对用的一个字符串。上述每个方法返回的值再用于初始化新对象的各个字段。
反序列化一个对象字段时,应调用和对象序列化时传递给AddValue方法的值的类型匹配的一个Get方法。换言之,如果GetObjectData方法调用AddValue时传递的一个Int32值,那么在反序列化对象时,应该为同一个值调用GetInt32方法。如果值再流中的类型和你试图获取的类型不符合,格式化器会尝试用一个IFormatterConverter对象将流中的值转型成你指定的类型。
前面说过,构造SerializationInfo对象时,要向它传递类型实现了IFormatterConverter接口的一个对象。由于格式化器负责构造SerializationInfo对象,所以要有他选择它想要的IFormatterConverter类型。微软的 BinaryFormatter,SoapFormatter类型总是构造FormatterConverter类型的一个实例,微软的格式化器没有提供任何方法让你选择一个不同的IFormatterConverter类型。
FormatterConverter类型调用System.Convert类的各种静态方法在不同的核心类型之间对值进行转换,比如将一个Int32转换成Int64。然而,为了在其他任何类型之间转换一个值,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的,调用它可以正确的序列化对象。除此之外,格式化器将特殊的构造函数已虚拟化。换言之,反序列化期间,格式化器会检查要实例化的类型。如果那个类型没有提供特殊构造器,格式化器就会扫描基类,知道它找到了实现特殊构造器的一个类。
重要提示:特殊构造器中的代码一般会从传给他的SerializationInfo对象中提取字段。提取了字段后,不能保证对象以完全序列化,所有特殊构造器中的代码不应尝试操纵它提取的对象。
如果你的类型必须访问提取的一个对象中的成员(比如调用方法),建议你的类型提供一个应用了OnDeserialized的方法,或者让你的类型实现了IDeserializationCallback.OnDeserialization。调用该方法时,所有对象的字段都以设置好,然而,对于多个对象来说,他们额OnDeserialized或OnDeserialization方法的调用顺序是没有保障的。所以,虽然字段可能已经初始化,但你仍然不知道被应用的对象是否已完全序列化好.
如何在基类没有实现ISerializable的前提下定义一个实现他的类型
前面讲过,ISerializable接口的功能非常强大,它允许一个类型完全控制如何对类型的实例进行序列化和反序列化。然而,这个功能是由代价的;现在,该类型还是负责它的基类型的所有字段的序列化。如果基类型也实现了ISerializable接口,那么对基类型的字段进行序列化时很容易的。只需要调用基类型的GetObjectData方法即可。
但有朝一日,你可能要定义一个类型来控制它的序列化,但它的基类没有实现ISerializable接口,在这种情况下,派生类必须手动序列化基类的字段,具体的做法是获取他们的值,并把这些值添加到SerializationInfo集合中。然后,在你的特殊构造器中还必须从结合中取出这些值,并以某种方式设置基类的字段。如果基类的字段时public或protected字段,那么这一切都很容易实现。但是,如果基类字段时private字段,就很难或根本不可能实现。
下面代码演示了正确实现ISerializable接口的GetObjectData方法和它的隐含的构造器,使基类的字段被序列化:
[Serializable] internal class Base { protected String m_name = "Jeff"; public Base() { } } [Serializable] internal class Derived : Base, ISerializable { private DateTime m_date = DateTime.Now; public Derived() { } [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)] private Derived(SerializationInfo info, StreamingContext context) { //为我们的类和基类获取可序列化的成员集合 Type baseType = this.GetType().BaseType; MemberInfo[] mi = FormatterServices.GetSerializableMembers(baseType, context); //从info对象反序列化基类的字段 for (Int32 i = 0; i < mi.Length; i++) { FieldInfo fi = (FieldInfo)mi[i]; fi.SetValue(this, info.GetValue(baseType.FullName + "+" + fi.Name, fi.FieldType)); } m_date = info.GetDateTime("Date"); } [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)] public virtual void GetObjectData(SerializationInfo info, StreamingContext context) { //为这个类型希望序列化的值 info.AddValue("Date", m_date); Type baseType = this.GetType().BaseType; MemberInfo[] mi = FormatterServices.GetSerializableMembers(baseType, context); for (Int32 i = 0; i < mi.Length; i++) { info.AddValue(baseType.FullName + "+" + mi[i].Name, ((FieldInfo)mi[i]).GetValue(this)); } } public override string ToString() { return String.Format("Name={0},Date={1}", m_name, m_date); } }
在上述代码中,有一个名为Base的基类,它只用Serializable定制的attribute进行了标识。从Base派生的是Derived类,它出了应用Serializable标识,还实现了ISerializable接口。为了使局面变的更加有趣,两个类都定义了m_name的一个String字段。调用SerializationInfo的AddValue方法时,不能添加多个同名的值。在上述代码中,解决这个问题的方案实在字段名前附加其类名作为前缀,从而对每个字段进行标识。例如,当GetObjectData方法调用AddValue来序列化Base的m_name字段时,写入的值的名称是Base+m_name。