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

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

2012-07-18 20:43  xiashengwang  阅读(1069)  评论(2编辑  收藏  举报

一,运行时序列化的作用

  序列化(Serialization)是将一个对象转换成一个字节流的过程。反序列化(Deserialization)是将一个字节流转换回一个对象的过程。在对象和字节流之间的转化是非常有用的机制。下面是一些例子。

●应用程序的状态可以轻松保存到一个磁盘或数据库中,并在应用程序下次运行时恢复。Asp.net就是利用序列化和反序列化来保存和恢复会话状态的。

●一组对象可轻松复制到剪贴板,在粘贴回同一个或另一个应用程序。事实上,Windows窗体和WPF就是利用了这个功能。

●一组对象可以克隆放到一边作为“备份”;与此同时,用户操纵一组“主”对象。

●一组对象可轻松地通过网络发给另一台机器上运行的进程。Microsoft .Net Framework 的Remoting(运程处理)架构会对按值封送(marshaled by value)的对象进行序列化和反序列化。这个技术还可用于跨越AppDomain边界发送对象。

除了上述应用,一旦将对象序列化成内存中的一个字节流,就可以用一些更有用的方式来处理数据,比如加密和压缩数据等。

二,序列化/反序列化快速入门

  先看一个简单的例子:

        private void QuickStartSerialization()
        {
            //创建一个对象图,以便把它序列化到流中
            var objectGraph = new List<string> { "Jeff", "Jim", "Jom" };
            Stream stream = SerializeToMemory(objectGraph);

            //为了演示,将一切重置
            stream.Position = 0;
            objectGraph = null;

            //反序列化,证明它能工作
            objectGraph = (List<string>)DeserializFromMemory(stream);
            foreach (var s in objectGraph)
            {
                Console.WriteLine(s);
            }
        }

        private Stream SerializeToMemory(object graph)
        {
            //构造一个流来容纳序列化的对象
            MemoryStream stream = new MemoryStream();

            //构造一个序列化格式化器,让它负责所有复杂的工作
            BinaryFormatter formatter = new BinaryFormatter();

            //告诉格式化器序列化对象到一个流中
            formatter.Serialize(stream, graph);

            //将序列化好的对象返回给调用者
            return stream;
        }
        private object DeserializFromMemory(Stream stream)
        {
            //构造一个序列化格式化器,让它负责所有复杂的工作
            BinaryFormatter formatter = new BinaryFormatter();

            //告诉格式化器从流中反序列化对象
            return formatter.Deserialize(stream);
        }

  代码注释部分已经写得很清楚了,需要注意的是,Serialize方法的第一个参数是一个流对象的引用,他表示对象序列化后应该放到哪里。第二个参数是一个对象的引用,这个对象可以是任何东西,如Int32,String,Exception,List<String>,Dictinary<string,Datetime>等等。格式化器参考对象类型的元数据,从而了解如何序列化完整的对象图。序列化时,Serialize方法利用反射来查看每个对象的类型中都有哪些字段。在这些字段中,任何一个引用了其他对象,格式化器的Serialize方法就知道那些对象也要序列化。格式化器非常智能,如果对象图中的两个对象相互引用,格式化器会检测到这一点,确保每个对象只被序列化一次,避免进入无限循环。

  Deserialize方法会检查流的内容,构造流中所有对象的实例,并初始化所有这些对象的字段。通常要将Deserialize返回的对象引用转换成应用程序期待的类型。

  序列化时的注意事项:

●首先,你必须保证代码为序列化和反序列化使用相同的格式化器。例如:不要用SoapFormatter序列化一个对象,再用BinaryFormatter反序列化。如果Deserialize发现自己解释不了一个流中的内容,就会抛出一个System.Runtime.Serialization.SerializationException异常。

●其次,可以将多个对象序列化到一个流中,这是一个很有用的操作。例如,假定有下面两个类:

        [Serializable]
        internal sealed class Customer { }
        [Serializable]
        internal sealed class Order { }

然后在应用程序主要类中,定义了以下字段:

        private List<Customer> m_customers = new List<Customer>();
        private List<Order> m_pendingOrders = new List<Order>();
        private List<Order> m_processedOrders = new List<Order>();

下面可以用如下方法将应用程序的状态序列化到单个流中:

        private void SaveApplicatonState(Stream stream)
        {
            BinaryFormatter formatter = new BinaryFormatter();
            //序列化应用程序的完整状态
            formatter.Serialize(stream, m_customers);
            formatter.Serialize(stream, m_pendingOrders);
            formatter.Serialize(stream, m_processedOrders);
        }

然后用下面的方法重建应用程序的状态:

        private void RestoreApplicationState(Stream stream)
        {
            BinaryFormatter formatter = new BinaryFormatter();
            //反序列化应用程序的完整状态(注意:和序列化的顺序一样)
            stream.Position = 0;
            m_customers = (List<Customer>)formatter.Deserialize(stream);
            m_pendingOrders = (List<Order>)formatter.Deserialize(stream);
            m_pendingOrders = (List<Order>)formatter.Deserialize(stream);
        }

●第三也是最后一点注意事项与程序集有关。序列化一个对象时,类型的全名和程序集的名称会被写入流。默认情况下,BinaryFormatter会输出程序集的完整标识,包括程序集的文件名,版本号,语言文化以及公钥信息。反序列化时,格式化器首先获得程序集的标识信息,并通过Assembly的Load方法加载程序集,确保程序集在正在执行的AppDomain中。程序集加载好后,格式化器在程序集中查找与要反序列化的对象匹配的一个类型。如果程序集不包含一个匹配的类型,就抛出一个异常,不再对更多的对象进行序列化。如果找到一个匹配的类型,就创建类型的一个实例,并以流中包含的值对字段进行初始化。如果类型中的字段与流中读取的字段名不完全匹配,就抛出一个SerializtionException异常,不再对更多的对象进行序列化。

重要提示:有的可扩展应用程序使用了Assembly.LoadFrom加载一个程序集,然后根据加载的程序集中的类型来构造对象。这些对象可以毫无问题的序列化到一个流中。然而,在反序列化是,格式化器会通过调用Assembly的Load方法(而非LoadFrom)来尝试加载程序集。在大多数情况下,CLR都无法定位程序集文件,将会抛出SerializatonException异常。许多开发人员为此感到不解。要解决这个问题,在调用格式化器的Deserialize方法之前,可以向System.AppDomain的AssemblyResolve事件注册一个System.ResoveEventHandler类型的委托方法,在这个方法中加载需要的程序集。在Deserialize方法返回后,马上注销这个委托方法。

  FCL提供了2个格式化器,BinaryFormatter和SoapFormatter,从.Net 3.5起,SoapFormatter类已被废弃,如果要生成XML的序列化可以使用XmlSerializer和DataContractSerializer类。

三,使类型可序列化

  设计一个类时,默认情况是不允许序列化的,要使类型可序列化,需要向类型应用一个名为System.SerializationAttribute特性。如:

        [Serializable]
        internal sealed class Customer { }

  注意:序列化一个对象时,有的对象也许能序列化,有的也许不能。考虑到性能,在序列化前,格式化器不会验证所有对象都能序列化。所以,序列化一个对象图时,在抛出SerializationException之前,完全有可能已经有一部分对象序列化到流中。如果发生这种情况,流中就包含已损坏的数据。如果你认为有些对象不可序列化,那么写的代码就应该能从这种情况中恢复。一个方案是,先将对象序列化到MemoryStream中,如果对象序列化成功,再将其复制到真正希望的目标流(比如文件或网络)。

  SerializationAttribute特性能够应用于引用类型(class),值类型(struct),枚举(enum)和委托(delegate)类型。注意,枚举和委托总是可序列化的,不必显示指定这个特性。除此之外,SerializationAttribute不会被派生类型继承。下面的类型是不可序列化的:

        [Serializable]
        internal sealed class Person { }
        internal sealed class Employee : Person { }

为解决这个问题,只需将SerializationAttribute应用于Employee类型。

        [Serializable]
        internal sealed class Person { }
        [Serializable]
        internal sealed class Employee : Person { }

上述问题很容易修正,但反之则不然。如果一个基类没有应用SerializationAttribute特性,那么很难想象如何从它派生出一个可序列化的类型。这样的设计是有原因的,如果基类型不允许它的实例序列化,它的子类就不能序列化,因为基对象是派生对象的一部分。这正是System.Object已经很体贴的应用了SerializationAttribute的原因。

四,控制序列化和反序列化

  将SerializationAttribute这个attribute应用于一个类型时,所有的实例字段(public,private,protected等)都会被序列化(在标记了[Serialization]特性的类型中,不要使用C#的“自动实现属性”来定义属性。这是因为字段名是由编译器自动生成的,而每次重新编译,生成的名称都不同)。然而,类型可能定义了一些不应序列化的实例字段,一般情况,有以下两个原因:

●字段含有反序列化后变得无效的信息。例如,假定一个对象包含一个Windows内核对象(如文件,进程,线程,信号量等)的句柄,在反序列化到另一进程或机器之后,就会失去意义。因为Windows内核对象是与进程相关的值。

●字段含有很容易计算的值。在这种情况下,要选出那些无需序列化的字段,减少要传输的数据,从而增强应用程序的性能。

  下面的例子使用System.NonSerializedAttribute来指明哪些类型的字段不应序列化。

        [Serializable]
        internal class Circle
        {
            private Double m_radius;
            [NonSerialized]
            private double m_area;

            public Circle(Double radius)
            {
                m_radius = radius;
                m_area = Math.PI * radius * radius;
            }
        }

上述代码保证了m_area字段不会被序列化,因为它应用了NonSerializedAttribute。注意这个特性只能应用于字段,而且会被派生类型继承。当然,可以向一个对象的多个字段应用这个特性。假定我们的代码像下面这样构造了一个Circle的实例:

Circle circle = new Circle(10);

在内部m_area被设置成了314.159的值。这个对象在序列化时,只有m_radius字段的值(10)才会写入流。这是我们希望的,但当反序列化成一个Circle对象时,就会遇到一个问题。反序列化对象的m_radius字段会被设置为10,但m_area字段会被初始化为0—而不是314.159。下面的代码演示如何修正这个问题。

        [Serializable]
        internal class Circle
        {
            private Double m_radius;
            [NonSerialized]
            private double m_area;

            public Circle(Double radius)
            {
                m_radius = radius;
                m_area = Math.PI * radius * radius;
            }

            [OnDeserialized]
            private void OnDeserialized(StreamingContext context)
            {
                m_area = Math.PI * m_radius * m_radius;
            }
        }

在修改后的版本中,包含了一个应用了System.Runtime.Serialization.OnDeserializedAttribute特性的方法。每次反序列化类型的一个实例,格式化器都会检查是否有一个应用了该特性的方法。如果是,则调用该方法。调用这个方法时,所有可序列化的字段都会被正确设置。在方法中,可能需要访问这些字段来执行一些额外的操作,从而确保对象的完全序列化。上述修改后,在OnDeserialized方法中我们使用了m_radius来计算m_area的值,这样一来,m_area就有了我们希望的值(314.159)。

  除了使用OnDeserializedAttribute,System.Runtime.Serialization命名空间还定义了OnDeserializingAttribute,OnSerializedAttribute和OnSerializingdAttribute特性。可将它们应用于类型中定义的方法,对序列化和反序列化进行更多的控制。

        [Serializable]
        public class MyType
        {
            Int32 x, y;
            [NonSerialized]
            Int32 sum;

            public MyType(Int32 x, Int32 y)
            {
                this.x = x; this.y = y; this.sum = x + y;
            }

            [OnDeserializing]
            private void OnDeserializing(StreamingContext context)
            {
                //示例:在这个类型的新版本中,为字段设置默认值
            }

            [OnDeserialized]
            private void OnDeserialized(StreamingContext context)
            {
                //示例:根据字段值初始化瞬时状态(比如sum的值)
                sum = x + y;
            }

            [OnSerializing]
            private void OnSerializing(StreamingContext context)
            {
                //示例:在序列化前,修改任何需要修改的状态
            }

            [OnSerialized]
            private void OnSerialized(StreamingContext context)
            {
                //示例:在序列化后,恢复任何需要恢复的状态
            }
        }

如果序列化一个类型的实例,在类型中添加了一个新的字段,然后试图反序列化不包含新字段的类型的对象,格式化器会抛出SerializationException异常,这非常不利于版本控制,因为我们经常需要在类型的一个新版本中添加新字段。幸好,这时可以利用OptionalFieldAttribute的帮助。类型中新增的每个字段都要应用OptionalFieldAttribute特性。然后,当格式化器看到该attribute应用于一个字段时,就不会因为流中的数据不包含这个字段而抛出SerializationException异常。

 

未完待续,下接《CLR via C#》笔记——运行时序列化(2)