UML软件工程组织

解析.Net框架下的序列化机制
王凯明 yesky
  程序员在编写应用程序的时候往往要将程序的某些数据存储在内存中,然后将其写入某个文件或是将它传输到网络中的另一台计算机上以实现通讯。这个将程序数据转化成能被存储并传输的格式的过程被称为"序列化"(Serialization),而它的逆过程则可被称为"反序列化"(Deserialization)。

  .Net框架对序列化机制具有非常好的支持,它提供了两个名字空间(namespace):System.Runtime.Serialization和System.Runtime.Serialization.Formatters以完成序列化机制的大部分功能。系列化这项技术可以应用在将程序产生的结果数据存储到文件系统中,但是它更主要的应用是在于.Net Remoting和Web服务的实现上。

  序列化机制的实现是依靠格式器(Formatter)而完成的,它是一个从System.Runtime.Serialization.IFormatter继承下来的类的对象。格式器完成了将程序数据转化到能被存储并传输的格式的工作,同时也完成了将数据转化回来的工作。.Net框架为程序员提供了两种类型的格式器,一种通常是应用于桌面类型的应用程序的,它一个是System.Runtime.Serialization.Formatters.Binary.BinaryFormatter类的对象,而另一种则更主要的应用于.Net Remoting和XML Web服务等领域的,它一个是System.Runtime.Serialization.Formatters.Soap.SoapFormatter类的对象。从它们的名称来看,我们不妨将它们分别称为二进制格式器和XML格式器。

  本文将从这两个格式器入手,先向大家介绍分别用它们如何实现序列化和反序列化,然后比较两种格式器的不同点。接着我会向大家介绍实现序列化对对象类型的一些要求,同时还要向大家介绍两种不同的序列化方式:基本序列化(Basic Serialization)和自定义序列化(Custom Serialization)。最后,我还会给大家介绍一个实例程序以加深大家对序列化机制的理解程度。

  一.二进制格式器(Binary Formatter) vs XML格式器(XML Formatter):

  下面我先向大家介绍两种不同的格式器,分别用它们如何实现序列化机制和反序列化机制,请看下面的代码:

#region Binary Serializers
public static System.IO.MemoryStream SerializeBinary(object request) {
 System.Runtime.Serialization.Formatters.Binary.BinaryFormatter serializer =
  new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
 System.IO.MemoryStream memStream = new System.IO.MemoryStream();
 serializer.Serialize(memStream, request);
 return memStream;
}

public static object DeSerializeBinary(System.IO.MemoryStream memStream) {
 memStream.Position=0;
 System.Runtime.Serialization.Formatters.Binary.BinaryFormatter deserializer =
  new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
 object newobj = deserializer.Deserialize(memStream);
 memStream.Close();
 return newobj;
}
#endregion

#region XML Serializers
public static System.IO.MemoryStream SerializeSOAP(object request) {
 System.Runtime.Serialization.Formatters.Soap.SoapFormatter serializer =
  new System.Runtime.Serialization.Formatters.Soap.SoapFormatter();
 System.IO.MemoryStream memStream = new System.IO.MemoryStream();
 serializer.Serialize(memStream, request);
 return memStream;
}

public static object DeSerializeSOAP(System.IO.MemoryStream memStream) {
 object sr;
 System.Runtime.Serialization.Formatters.Soap.SoapFormatter deserializer =
  new System.Runtime.Serialization.Formatters.Soap.SoapFormatter();
 memStream.Position=0;
 sr = deserializer.Deserialize(memStream);
 memStream.Close();
 return sr;
}
#endregion

  从上面的代码我们可以发现无论运用哪种格式器,其基本的过程都是一样的,而且都是非常容易实现的,唯一的不同就是定义格式器的类型不同。不过在实际的应用中,二进制格式器往往应用于一般的桌面程序和网络通讯程序中,而XML格式器禀承了XML技术的优点,大多数被应用于.Net Remoting和XML Web服务等领域。下面我们来分析一下两种格式器各自的优点。

  二进制序列化的优点:

  1. 所有的类成员(包括只读的)都可以被序列化;

  2. 性能非常好。

  XML序列化的优点:

  1. 互操作性好;

  2. 不需要严格的二进制依赖;

  3. 可读性强。

  通过分析上面的代码,我们知道了选择二进制序列化的方式还是选择XML序列化的方式仅仅是对不同的格式器进行选择而已。你可以根据实际的需要选择相应的格式器完成序列化和反序列化工作。同时请注意,代码中的序列化函数和反序列化函数仅仅是在调用Serialize()和Deserialize()这两个核心函数上产生了差别,即它们的参数不同。因此以上的代码完成了一些最最基本但是很重要的功能,你可以将它们运用在你的程序中,或是将其进行适当扩充以满足程序的特定需要。

  二.序列化机制对类的要求:

  如果你要对一个对象进行序列化,那么你必须将它的类型标记为[Serializable()],该操作是通过SerializableAttribute属性来实现的。将SerializableAttribute属性应用于一种数据类型可表明该数据类型的实例可以被序列化。如果正在序列化的对象图中的任何类型未应用SerializableAttribute属性,公共语言运行库则会引发SerializationException。默认情况下,类型中由SerializableAttribute标记的所有公共和私有字段都会进行序列化,除非该类型实现ISerializable接口来重写序列化进程(通过实现该接口我们便可以实现将在后面介绍的"自定义序列化")。默认的序列化进程会排除用NonSerializedAttribute属性标记的字段,即你可以将该类型标记为[NonSerialized()]以表明它是不可以被序列化的。如果可序列化类型的字段包含指针、句柄或其他某些针对于特定环境的数据结构,并且不能在不同的环境中以有意义的方式重建,则最好将NonSerializedAttribute属性应用于该字段。有关序列化的更多信息,请参阅System.Runtime.Serialization名字空间中的相关内容。

  下面我给大家介绍一个例子,以显示如何正确的运用SerializableAttribute属性和NonSerializedAttribute属性。该程序中运用到了XML格式器,不过同时给出了二进制格式器为参考(程序中将其用"//"标注),其实现的结果是一样的。该程序实现的功能是在序列化和反序列化操作前后测试对象因包含了[NonSerialized()]的字段而显示不同的屏幕打印结果。其代码如下:

using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Soap;
//using System.Runtime.Serialization.Formatters.Binary;

public class Test {
 public static void Main() {
  // 创建一个新的测试对象
  TestSimpleObject obj = new TestSimpleObject();

  Console.WriteLine("Before serialization the object contains: ");
  obj.Print();

  // 创建一个文件"data.xml"并将对象序列化后存储在其中
  Stream stream = File.Open("data.xml", FileMode.Create);
  SoapFormatter formatter = new SoapFormatter();
  //BinaryFormatter formatter = new BinaryFormatter();

  formatter.Serialize(stream, obj);
  stream.Close();
  
  // 将对象置空
  obj = null;

  // 打开文件"data.xml"并进行反序列化得到对象
  stream = File.Open("data.xml", FileMode.Open);
  formatter = new SoapFormatter();
  //formatter = new BinaryFormatter();

  obj = (TestSimpleObject)formatter.Deserialize(stream);
  stream.Close();

  Console.WriteLine("");
  Console.WriteLine("After deserialization the object contains: ");
  obj.Print();
 }
}

// 一个要被序列化的测试对象的类
[Serializable()]
 public class TestSimpleObject {
 public int member1;
 public string member2;
 public string member3;
 public double member4;

 // 标记该字段为不可被序列化的
[NonSerialized()] public string member5;

public TestSimpleObject() {
 member1 = 11;
 member2 = "hello";
 member3 = "hello";
 member4 = 3.14159265;
 member5 = "hello world!";
}

public void Print() {
 Console.WriteLine("member1 = '{0}'", member1);
 Console.WriteLine("member2 = '{0}'", member2);
 Console.WriteLine("member3 = '{0}'", member3);
 Console.WriteLine("member4 = '{0}'", member4);
 Console.WriteLine("member5 = '{0}'", member5);
}
}

  三.基本序列化(Basic Serialization) vs 自定义序列化(Custom Serialization):

  .Net框架为我们提供了两种方式的序列化:一种为基本序列化、另一种为自定义序列化。值得注意的是,序列化的方式和前面提到的序列化的格式是不同的概念。序列化的方式是指.Net框架将程序的数据转化为能被存储并传输的格式的实际过程,它是不管程序员运用了何种类型的格式器的(二进制格式器还是XML格式器)。而序列化的格式则指程序的数据是被转化成二进制格式了还是被转化成XML格式了。

  完成序列化的最简单的方法便是让.Net框架自动为我们完成整个过程,而我们不必去管它内部是如何具体实现的,这种方法便是前面提到的"基本序列化"。在这种方式下,我们需要做的仅仅是将类标记上[Serializable()]属性。然后.Net框架便调用该类的对象并将它转化为所需的格式。同时你还可以控制其中的某些字段不被序列化,方法就是前面所述的将该字段标记上[NonSerialized()]属性。这样,最最简单和基本的序列化工作就完成了,不过其内部是如何实现的你是不得而知的,同时你也不能进一步控制序列化过程的程序行为。

  如果你要获得对序列化的更大的控制权,那么你就得使用"自定义序列化"的方式。通过使用这种方式,你可以完全的控制类的哪些部分能被序列化而哪些部分不能,同时你还可以控制如何具体的进行序列化。运用该方式的好处就是能克服基本序列化所会遇到的问题。我们在运用基本序列化将一个类的对象序列化完毕并存储在文件中后,假设该对象原来有三个字段,如果此时该对象增加了一个字段,那么再将该对象从文件中反序列化出来时会发生字段数不一致的错误。这样的问题是基本序列化所不能解决的,只能运用自定义序列化的方式来解决。

  在介绍自定义序列化之前,我先给出介绍过程中所要用到的实例程序的代码。这是一个时间安排程序,其中要用到将不同的时间格式进行转化的操作。所以运用序列化的机制能很好的解决这个问题。

using System;
using System.Runtime.Serialization;

namespace SerializationSample {
[Serializable()]
 public class Schedule {
  protected System.DateTime start;
  protected System.DateTime end;
  // 每个时间间隔所要增加的毫秒数
  protected long interval;

  public System.DateTime Start {get{return start;}set{start=value;}}
  public System.DateTime End {get{return end;}set{end=value;}}
  public long Interval {get{return interval;}set{interval=value;}}
  public Schedule(System.DateTime Start, System.DateTime End, long Interval) {
   start=Start;
   end=End;
  interval=Interval;
}

// 如果已经到了结束的时间,则返回结束时间,否则返回下一次运行的时间
public System.DateTime NextRunTime {
 get {
  System.TimeSpan ts = new System.TimeSpan(end.Ticks-System.DateTime.Now.Ticks);
  if(ts.Milliseconds>0) {
   return System.DateTime.Now.AddMilliseconds(interval);
  } else {
   return end;
  }
 }
}
}
}

  自定义序列化:

  下面我就向大家介绍自定义序列化以及反序列化的具体过程。首先,程序的类必须实现System.Runtime.Serialization.ISerializable接口,该接口的功能就是允许对象控制其自己的序列化和反序列化过程。所以我们得重新定义上面的类:

[Serializable()]
public class ScheduleCustom : System.Runtime.Serialization.Iserializable

  接下来,我们必须对该接口调用GetObjectData()的实现,也即我们必须在上面的类中给出GetObjectData()的具体实现。其函数原型如下:

void GetObjectData(SerializationInfo info, StreamingContext context);

  上面的类中GetObjectData()的具体实现如下:

public void GetObjectData(SerializationInfo info,StreamingContext context) {
// 运用info对象来添加你所需要序列化的项
info.AddValue("start", start);
info.AddValue("end", end);
info.AddValue("interval", interval);
}

   然而对于这么一个简单的方法,读者可能不能理会到系列化带来的强大功能,所以下面我就给这个方法添加一些东西。如果在系列化过程中我们要查看类型为DateTime的"start"属性的输出的话,其结果会是.Net框架默认的格式:

  2002-12-19T14:09:13.3457440-07:00

  而对于没有.Net框架的用户,或是在其他时间区域内的用户而言,这么一个格式的时间可能是非常难以理解的,所以我们有必要将时间的格式转化为格林威治标准时间格式,于是修改GetObjectData()方法如下:

public void GetObjectData(SerializationInfo info,StreamingContext context) {
// 运用info对象来添加你所需要序列化的项
// 同时,将"start"和"end"属性的时间格式转化为格林威治标准时间格式
info.AddValue("start", System.TimeZone.CurrentTimeZone.ToUniversalTime(start));
info.AddValue("end", System.TimeZone.CurrentTimeZone.ToUniversalTime(end));
info.AddValue("interval", interval);
info.AddValue("timeformat", "utc");
}

  这样一来,我们在系列化过程中查看"start"属性时就会得到如下结果:

   8/19/2002 9:09:13 PM

  同时请注意我们在GetObjectData()方法中添加的一个名为"timeformat"的额外属性,通过它我们可以方便的知道系列化过程中所使用的时间格式。如果有兴趣的话,你还可以从System.Globalization.DateTimeFormatInfo这个名字空间中获取更多有关时间格式的信息。

  自定义反序列化:

  你可以通过调用一个自定义的构造函数来完成自定义反序列化的操作。该构造函数的定义如下:

public ScheduleCustom (SerializationInfo info,StreamingContext context);

  在上面的类中,我们的ScheduleCustom()方法将完成把时间格式从格林威治标准时间格式反序列化为当地时间的格式的操作,其函数实现如下:

public ScheduleCustom (SerializationInfo info,StreamingContext context) {
 this.start = info.GetDateTime("start").ToLocalTime();
 this.end = info.GetDateTime("end").ToLocalTime();
 this.interval = info.GetInt32("interval");
}

  在完成自定义序列化和自定义反序列化后,我们的时间安排程序变成了如下形式:

using System;
using System.Runtime.Serialization;

namespace SerializationSample {
[Serializable()]
 public class ScheduleCustom : System.Runtime.Serialization.ISerializable {
 protected System.DateTime start;
 protected System.DateTime end;
 // 每个时间间隔所要增加的毫秒数
 protected long interval;

 public System.DateTime Start {get{return start;}set{start=value;}}
 public System.DateTime End {get{return end;}set{end=value;}}
 public long Interval {get{return interval;}set{interval=value;}}

 public ScheduleCustom(System.DateTime Start, System.DateTime End, long Interval) {
  start=Start;
  end=End;
  interval=Interval;
 }

// 如果已经到了结束的时间,则返回结束时间,否则返回下一次运行的时间
public System.DateTime NextRunTime {
 get {
  System.TimeSpan ts = new System.TimeSpan(end.Ticks-System.DateTime.Now.Ticks);
  if(ts.Milliseconds>0) {
   return System.DateTime.Now.AddMilliseconds(interval);
  } else {
   return end;
  }
 }
}

public void GetObjectData(SerializationInfo info,StreamingContext context) {
 // 运用info对象来添加你所需要序列化的项
 // 同时,将"start"和"end"属性的时间格式转化为格林威治标准时间格式
 info.AddValue("start", System.TimeZone.CurrentTimeZone.ToUniversalTime(start));
 info.AddValue("end", System.TimeZone.CurrentTimeZone.ToUniversalTime(end));
 info.AddValue("interval", interval);
 info.AddValue("timeformat", "utc");
}

public ScheduleCustom (SerializationInfo info,StreamingContext context) {
 this.start = info.GetDateTime("start").ToLocalTime();
 this.end = info.GetDateTime("end").ToLocalTime();
 this.interval = info.GetInt32("interval");
 }
}
}

  四.总结:

  本文向大家介绍了.Net框架下系列化机制的一些基本概念和基本的运用方法,读者在读完本文后,应该对以下几个概念有个初步了解:二进制系列化、XML系列化、基本序列化和自定义系列化,并应能够完成一些基本的系列化应用。最后,希望大家能合理有效的运用系列化机制并发挥它的功效以更好地满足实际工作需要。


版权所有:UML软件工程组织