雁过请留痕...
代码改变世界

《CLR via C#》笔记——运行时序列化(2)

2012-07-18 20:52  xiashengwang  阅读(1422)  评论(0编辑  收藏  举报

五,格式化器如何序列化类型实例

为了简化格式化的操作,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)