zoukankan      html  css  js  c++  java
  • Effective C# Item25:尽可能将类型实现为可序列化的类型

        持久化是类型的一个核心特性,有时我们需要通过不同的方式传输和创建同一个对象,例如需要通过网络传输对象,或者需要将对象信息存储到文本文件或者XML文件中,这时,如何能够保持对象的状态,并在将来使用时,可以准确的还原到原来的状态,是非常重要的。

        .NET可以使用序列化的方式来持久化对象,我们在可能的情况下,应该将类型定义为可以序列化的。序列化时,我们可以使用Serializable特性。

        我们来看下面的简单示例,将对象信息存储到XML文件中。

        首先定义两个可以序列化的类型。

    代码
    1 [Serializable]
    2 public class Employee
    3 {
    4 private PersonName m_Name;
    5 public PersonName Name
    6 {
    7 get { return m_Name; }
    8 set { m_Name = value; }
    9 }
    10
    11 private string m_strAddress;
    12 public string Address
    13 {
    14 get { return m_strAddress; }
    15 set { m_strAddress = value; }
    16 }
    17
    18 public Employee()
    19 {
    20 m_Name = new PersonName();
    21 m_strAddress = string.Empty;
    22 }
    23
    24 public Employee(PersonName name, string address)
    25 {
    26 m_Name = name;
    27 m_strAddress = address;
    28 }
    29
    30 public override string ToString()
    31 {
    32 return string.Format("Name : {0} {1}, Address : {2}", m_Name.FirstName, m_Name.LastName, m_strAddress);
    33 }
    34 }
    35
    36 [Serializable]
    37 public class PersonName
    38 {
    39 private string m_strFirstName;
    40 public string FirstName
    41 {
    42 get { return m_strFirstName; }
    43 set { m_strFirstName = value; }
    44 }
    45
    46 private string m_strLastName;
    47 public string LastName
    48 {
    49 get { return m_strLastName; }
    50 set { m_strLastName = value; }
    51 }
    52
    53 public PersonName()
    54 {
    55 m_strFirstName = string.Empty;
    56 m_strLastName = string.Empty;
    57 }
    58
    59 public PersonName(string firstName, string lastName)
    60 {
    61 m_strFirstName = firstName;
    62 m_strLastName = lastName;
    63 }
    64 }
        下面是测试方法,包含了序列化和反序列化的过程。

    代码
    1 private static void Test()
    2 {
    3 Employee emp = new Employee(new PersonName("Wing", "Lee"), "BeiJing");
    4 Console.WriteLine("Ouput emp info before serialize:");
    5 Console.WriteLine(emp.ToString());
    6
    7 XmlSerializer serializer = new XmlSerializer(typeof(Employee));
    8 StreamWriter writer = new StreamWriter("emp.xml");
    9 serializer.Serialize(writer, emp);
    10 writer.Close();
    11 Console.WriteLine("Serialization Success.");
    12
    13 StreamReader reader = new StreamReader("emp.xml");
    14 XmlSerializer desrializer = new XmlSerializer(typeof(Employee));
    15 object o = desrializer.Deserialize(reader);
    16 if (o is Employee)
    17 {
    18 Console.WriteLine("Ouput emp info after deserialize:");
    19 Console.WriteLine((o as Employee).ToString());
    20 }
    21 }
        上述代码的执行结果如下所示。

        可以看到序列化前和序列化后的对象信息,被完全一致的反应出来,这说明,序列化确实实现了对象的持久化。

        有以下两个问题需要注意:

    1. 对于可以序列化的类型,必须提供没有参数的构造函数,上述代码中,如果Employee类型和PersonName类型没有显示的提供默认构造函数,那么程序在编译时,就会报错,提示在不提供没有参数的构造函数的情况下,无法执行序列化操作。
    2. 出于性能的考虑,我们可以不用将类型全部内容都置为可序列化。

        C#在序列化时,可以采用上面代码中写的XmlSerializer的方式,也可以采用BinaryFormatter的方式,如果采用XmlSerializer的方式,那么NonSerialized特性是不会发挥作用的,同时这种方式允许序列化中的类型包含不能序列化的类型;但是对于BinaryFormatter来说,可以使用NonSerialized特性,同时进行序列化的类型所包含的其他所有类型,都必须是可序列化的,否则就会在序列化的过程中发生异常。

        我们来看以下的代码,使用Formatter的正常方式。

    代码
    1 private static void TestWithFormatter()
    2 {
    3 Employee emp = new Employee(new PersonName("Wing", "Lee"), "BeiJing");
    4 Console.WriteLine("Ouput emp info before serialize:");
    5 Console.WriteLine(emp.ToString());
    6
    7 BinaryFormatter serializer = new BinaryFormatter();
    8 Stream stream = File.Open("emp.txt", FileMode.Create);
    9 serializer.Serialize(stream, emp, null);
    10 stream.Close();
    11 Console.WriteLine("Serialization Success.");
    12
    13 stream = File.Open("emp.txt", FileMode.Open);
    14 BinaryFormatter deserializer = new BinaryFormatter();
    15 object o = deserializer.Deserialize(stream, null);
    16 stream.Close();
    17 if (o is Employee)
    18 {
    19 Console.WriteLine("Ouput emp info after deserialize:");
    20 Console.WriteLine((o as Employee).ToString());
    21 }
    22 }
        我们来修改一下Employee类型的代码,将m_strAddress字段用NonSerialized特性进行修饰,然后执行上述Test()方法和TestWithFormatter()方法,其中,Test()方法的执行结果是不会改变的;但是TestWithFormatter()方法的执行结果如下所示。

        我们可以看到,对于使用了NonSerilized特性的字段来说,在反序列化后,新生成的对象中相应字段的值时null或者0,为了解决这个问题,我们可以实现IDeserializationCallBack接口,来对序列化后新生成的对象的字段进行初始化。

        来看以下代码。

    代码
    1 [Serializable]
    2 public class Employee : IDeserializationCallback
    3 {
    4 private PersonName m_Name;
    5 public PersonName Name
    6 {
    7 get { return m_Name; }
    8 set { m_Name = value; }
    9 }
    10
    11 [NonSerialized]
    12 private string m_strAddress;
    13 public string Address
    14 {
    15 get { return m_strAddress; }
    16 set { m_strAddress = value; }
    17 }
    18
    19 public Employee()
    20 {
    21 m_Name = new PersonName();
    22 m_strAddress = string.Empty;
    23 }
    24
    25 public Employee(PersonName name, string address)
    26 {
    27 m_Name = name;
    28 m_strAddress = address;
    29 }
    30
    31 public override string ToString()
    32 {
    33 return string.Format("Name : {0} {1}, Address : {2}", m_Name.FirstName, m_Name.LastName, m_strAddress);
    34 }
    35
    36 public void OnDeserialization(object sender)
    37 {
    38 this.Address = "Vernus";
    39 }
    40 }
        上述代码实现了IDeserializationCallBack接口,在接口的OnDeserialization()方法中,我们对Address属性进行重新赋值,在代码修改后,TestWithFormatter()方法的执行结果如下图所示。

        我们在编码的过程中,可能在将对象序列化后,又修改了类型的结构,这时,单纯采用Serialzable特性,是不能对修改进行区分的,甚至可能会引发异常。这时,我们可以实现ISerializable接口,来定制序列化过程。

        我们来看下面的代码。

    代码
    1 [Serializable]
    2 public class Employee : IDeserializationCallback, ISerializable
    3 {
    4 private PersonName m_Name;
    5 public PersonName Name
    6 {
    7 get { return m_Name; }
    8 set { m_Name = value; }
    9 }
    10
    11 [NonSerialized]
    12 private string m_strAddress;
    13 public string Address
    14 {
    15 get { return m_strAddress; }
    16 set { m_strAddress = value; }
    17 }
    18
    19 public Employee()
    20 {
    21 m_Name = new PersonName();
    22 m_strAddress = string.Empty;
    23 }
    24
    25 public Employee(PersonName name, string address)
    26 {
    27 m_Name = name;
    28 m_strAddress = address;
    29 }
    30
    31 private Employee(SerializationInfo info, StreamingContext context)
    32 {
    33 Name = info.GetValue("name", typeof(PersonName)) as PersonName;
    34 Address = info.GetString("address");
    35 }
    36
    37 public void GetObjectData(SerializationInfo info, StreamingContext context)
    38 {
    39 info.AddValue("name", Name);
    40 info.AddValue("address", Address);
    41 }
    42
    43 public void OnDeserialization(object sender)
    44 {
    45 this.Address = "Vernus";
    46 }
    47
    48 public override string ToString()
    49 {
    50 return string.Format("Name : {0} {1}, Address : {2}", m_Name.FirstName, m_Name.LastName, m_strAddress);
    51 }
    52 }
        上述代码在执行TestWithFormatter()方法后的结果没有发生变化。我们在ISerializable接口的GetObjectData()方法中,以键/值对的方式将类型中的数据项进行存储,同时添加了一个构造函数,用于得到反序列化后对象中各数据项的值。

        一般情况下,我们对实现了ISerializable接口的类声明为sealed,这样可以免于被继承,因为继承后的子类,还要针对自身的数据情况,对GetObjectData()方法进行扩充,比较复杂。

        编程时要注意,从序列化流中写入和读取数据的顺序必须一致。

        总结:.NET框架为对象序列化提供了一个简单、标准的算法。如果我们的类型需要持久化,那就应该遵循标准的实现。如果我们不为类型添加序列化支持,那么其他使用我们类型的类也就不能支持序列化。我们所做的工作应该尽可能的使类型的使用者更加方便。如果可以,应该使用默认的方式来支持序列化;如果默认的Serializable特性不能够满足要求,则应该实现ISerializable接口。

  • 相关阅读:
    BZOJ2253: [2010 Beijing wc]纸箱堆叠
    解题:CF1055F Tree and XOR
    解题:JSOI 2011 柠檬
    解题:NOI 2009 诗人小G
    2019.2.28&2019.3.1 考试
    省选前作业题汇总2
    解题:LNOI 2014 LCA
    省选前作业题汇总1
    2019.2.26 考试
    解题:SDOI 2014 重建
  • 原文地址:https://www.cnblogs.com/wing011203/p/1651160.html
Copyright © 2011-2022 走看看