串行化(serialization)是指将一个对象的当前状态转换成字节流(a stream of bytes)的过程,而反串行化(deserialization)则指串行化过程的逆过程,将字节流转换成一个对象。初听起来可能对此不太感兴趣,但是使用串行化却有许多重要的原因。一旦将某一对象串行化,得到的字节可以存储在文件、数据库,或内存中 ——只要是可以存储的任何地方。需要恢复对象时,仅仅只需从它存储的位置反串行化即可。对象固有的这种特性对于无状态的Web应用程序是非常重要的,因为它允许重要的状态信息可以在用户请求之间保留。


串行化也允许对象在应用程序边界之间传递,编程人员可以将描述对象状态的字节流在网络上传递,并在另一端反串行化成一个匹配的对象。从本质上讲,串行化允许对象以“数值”的方式传递给另一个应用程序。.NET远程框架(将在第3章中讨论)广泛地在进程之间使用串行化来编组对象数据。


串行化不是.NET新引入的概念,Windows程序员同各种各样的串行化打交道:MFC中的归档机制、Visuabl Basic的PropertyBag和COM的IPersistStream接口等,都具有类似的特性。那么.NET串行化与这些方式又有什么区别呢?一个字,简单。在.NET以前的开发环境中,串行化是一个手工定制,对于类设计人员来说,实现类的串行化并不困难,但是实现难度较大的设计,它就显得能麻烦、耗时。 在.NET中,该过程完全是自动进行的,用户所做的就是使用Serializable特性来标记一个能串行化的类,运行库做余下的事。


.NET运行库怎样实现这一捷径的呢?当然是利用镜像,或更为准确地说是反射。前面提到,程序集含有名为元数据的自描述(self-describing)数据,程序集的元数据完整地表述了内部每一个类型,包括类型中每个私有字段的信息。通过反射过程,运行库能识别元数据,并用它来决定类应怎样被串行化。


2.6.1  使用Serializable特性

前面已经提到,如果允许运行库串行化一个对象,必须将该类标记上Serializable特性。下面的代码定义了两个标记了Serialable特性的类。

[Serializable]
public class Car
{
private string mColor;
private int mTopSpeed;
 
//Reference to another object
private Radio mRadio;
 
[NonSerialized] //Runtime will not serialize this field
private string mNickName;
 
public Car(string nickName, int topSpeed, string color)
{
mNickName = nickName;
mTopSpeed = topSpeed;
mColor = color;
mRadio = new Radio();
}
}
[Serializable]
public class Radio
{
private int mVolume = 5;
}

如上所示,Car类的mNickName字段用NonSerialized特性标注,指示运行库进行串行化时跳过该项,而其他部分允许进行串行化。这里注意,mRadio是对另一对象的引用,意味着运行库必须对其串行化。事实上,当运行库被告知要对某个对象串行化时,使用给定的对象作为根,建立一个对象图表(类似于垃圾回收器)。所有在图表中的对象也必须可被串行化的,否则运行库将引发异常。

下面的代码举例说明如何将Car对象串行化到一个文件之中。

static void Main(string[] args)
{
Car myCar = new Car("Christine", 150, "Red");
FileStream mySoapFile = File.Create("Car.txt");
 
// Use a SOAP formatter object to serialize the object.
new SoapFormatter().Serialize(mySoapFile, myCar);
 
mySoapFile.Close();
}

运行库不控制串行化数据的实际格式,但是有一个专门进行格式化的对象负责对串行化输出进行格式化。.NET框架有两种格式化程序:SOAP格式化程序和二进制格式化程序。在前面的例子中,SoapFormatter对象将对象串行化进一个SOAP报文,并将其发送到特定的流中。图2-26显示了在执行完这段代码后Car.txt中的内容。

如果仔细查看图2-26,很容易发现SOAP是如何描绘每个对象和它的数据成员。这种方式通过增加文件大小来提高了可读性。二进制格式化程序使用了更为紧凑的格式。如果需要在网络间发送对象,这种方式就更重要,尤其是在需要考虑网络带宽而不强调可读性的情况下。

通过调用SoapFormatter.Dewerialize方法,就可将该对象读入内存中:

static void Rehydrate()
{
FileStream mySoapFile = File.Open("Car.txt", FileMode.Open);
 
Car myCar = (Car) new SoapFormatter().Deserialize(mySoapFile);
 
mySoapFile.Close();
}



2.6.2  ISerializable接口和Formatter类

在某些情况下,需要比默认的串行化机制更强的功能时,可以实现ISerializable接口。它提供了一种对串行化数据更为精确控制,但不提供对数据格式的控制。这时,就需要扩展Formatter类。

下面的代码演示了如何实现ISerializable接口。

[Serializable]
public class Car : ISerializable
{
private string mColor;
static private int mTopSpeed;
private Radio mRadio;
 
[NonSerialized]
private string mNickName;
 
// Required by the ISerializable interface
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.AddValue("mColor", mColor);
info.AddValue("mTopSpeed", mTopSpeed);
info.AddValue("mRadio", mRadio);
}
 
// This contructor is required todeserialize the object
private Car(SerializationInfo info, StreamingContext context)
{
mTopSpeed = info.GetInt32("mTopSpeed");
mColor = info.GetString("mColor");
mRadio = (Radio) info.GetValue("mRadio", typeof(Radio));
}
. . .
}

当运行库串行化对象时,将调用ISerializable.GetObjectData方法,并传递SerializationInfo对象和StreamingContext结构。SerializationInfo对象提供AddValue方法,允许在对象中使用相关的名称存储一个值。VB6.0编程人员也许认为SerializationInfo对象是PropertyBag的另一个表示方法,但是与PropertyBag仅存储变量不同,SerializationInfo对象能存储强类型数据。AddValue经过几次重载,允许加入任何一个原始CLR类型。可以研究StreamingContext结构的内容,从而准确地判定加速串行化处理过程的因素是什么。例如,如果一个对象被传递给另一处理过程时,希望采用不同的串行化该对象的方法。如下代码所示:

public void GetObjectData(SerializationInfo info, StreamingContext context)
{
// If serializing across proxess , then skip the radio object
if (context.State != StreamingContextStates.CrossProcess)
{
mRadio = (Radio) info.GetValue("mRadio", typeof(Radio));
}
info.AddValue("mColor", mColor);
info.AddValue("mTopSpeed", mTopSpeed);
}

该例考察StreamingContext结构的State字段。这个枚举指定了被串行化对象的源或目标地。如果它与StreamingContextStates.CrossProcess相同,目的地就是同一机器上的另一进程。在那种情况下,代码不能对包含的Radio对象串行化。

通过对ISerializable接口的实现允许控制“什么”被串行化。而控制对象“如何”被串行化,就必须采用格式化程序类。这里还需要实现其他几种方法,包括Serialize、Deserialize、WriteBoolean、WriteByte、WriteArray等。这比本书实际用到的要深,因此把它留给细心的读者作为练习。


2.7  小结

本章目的是探讨对于分布式程序设计十分重要的.NET基本概念,具体包括:

●      通过构建CTS和CLS,.NET实现了语言和平台的互操作性。

●      CLR是对CTS的实现的描述。它是一种为.NET程序提供的围绕Windows操作系统和运行环境的面向对象包装。

●      .NET程序集表示一个可控制版本的部署单元。它提供了包含所有定义类型的容器。强名称程序集可用于单个应用程序(私有程序集),或将其安装在GAC中,供其他应用程序使用(共享程序集)。

●      特性为代码项提供附加的信息。有些特性能被编译器和运行库识别,并能指导它们对代码项完成特殊的操作。自定义特性可以自己生成,如果被检测到并通过反射实现操作时是很有用的。

●      .NET使用垃圾回收实现资源管理,代替了在COM中使用的引用计数。垃圾回收虽然能很好解决循环引用问题并在多线程环境中提高运行性能,但是不像引用计数,不能提供确定性终止。

●      通过反射,.NET提供了自动类型串行化。如果希望由运行库实现串行化,只需要在希望串行化的对象前使用Serializable特性;并且可通过实现ISerializable接口来控制串行化过程。

当然,.NET的内容很多,不可能在一章中全部涉及。希望通过本章的介绍,为读者在本书后续部分的学习过程中提供必要的知识储备。