zoukankan      html  css  js  c++  java
  • 《CLR via C#》笔记——运行时序列化(1)

    一,运行时序列化的作用

      序列化(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)

  • 相关阅读:
    和至少为 K 的最短子数组
    使用VS code编写C++无法实时检测代码的解决办法
    anaconda安装VSCODE后,python报错
    神经网络中sigmod函数和tanh函数的区别
    获取本机IP
    windows C++捕获CMD命令输出
    windows下面生成 dump
    windows 控制台命令输出 捕获
    不使用PEM 文件建立SSL通道
    OpenSSL socket 服务端
  • 原文地址:https://www.cnblogs.com/xiashengwang/p/2598108.html
Copyright © 2011-2022 走看看