荔园在线

荔园之美,在春之萌芽,在夏之绽放,在秋之收获,在冬之沉淀

[回到开始] [上一篇][下一篇]


发信人: baty (新一代懒人), 信区: DotNET
标  题: .NET 中的对象序列化(zz)
发信站: 荔园晨风BBS站 (Tue Nov 27 11:30:42 2001), 转信

.NET 中的对象序列化
Piet Obermeyer
Microsoft Corporation
2001 年 8 月
摘要:为什么要使用序列化?最重要的两个原因是:将对象的状态保存在存储媒体

中以
便可以在以后重新创建出完全相同的副本;按值将对象从一个应用程序域发送至另

一个
应用程序域。例如,序列化可用于在 ASP.NET 中保存会话状态,以及将对象复制

到 Wi
ndows 窗体的剪贴板中。它还可用于按值将对象从一个应用程序域远程传递至另一

个应
用程序域。本文简要介绍了 Microsoft .NET 中使用的序列化。
目录
简介
简介
持久存储
按值封送
基本序列化
选择性序列化
自定义序列化
序列化过程的步骤
版本控制
序列化规则
简介
序列化是指将对象实例的状态存储到存储媒体的过程。在此过程中,先将对象的公

共字
段和私有字段以及类的名称(包括类所在的程序集)转换为字节流,然后再把字节

流写
入数据流。在随后对对象进行反序列化时,将创建出与原对象完全相同的副本。
在面向对象的环境中实现序列化机制时,必须在易用性和灵活性之间进行一些权衡

。只
要您对此过程有足够的控制能力,就可以使该过程在很大程度上自动进行。例如,

简单
的二进制序列化不能满足需要,或者,由于特定原因需要确定类中那些字段需要序

列化
。以下各部分将探讨 .NET 框架提供的可靠的序列化机制,并着重介绍使您可以根

据需
据需
要自定义序列化过程的一些重要功能。
持久存储
我们经常需要将对象的字段值保存到磁盘中,并在以后检索此数据。尽管不使用序

列化
也能完成这项工作,但这种方法通常很繁琐而且容易出错,并且在需要跟踪对象的

层次
结构时,会变得越来越复杂。可以想象一下编写包含大量对象的大型业务应用程序

的情
形,程序员不得不为每一个对象编写代码,以便将字段和属性保存至磁盘以及从磁

盘还
原这些字段和属性。序列化提供了轻松实现这个目标的快捷方法。
公共语言运行时 (CLR) 管理对象在内存中的分布,.NET 框架则通过使用反射提供

自动
的序列化机制。对象序列化后,类的名称、程序集以及类实例的所有数据成员均被

写入
存储媒体中。对象通常用成员变量来存储对其他实例的引用。类序列化后,序列化

引擎
将跟踪所有已序列化的引用对象,以确保同一对象不被序列化多次。.NET 框架所

提供的
序列化体系结构可以自动正确处理对象图表和循环引用。对对象图表的唯一要求是

,由
正在进行序列化的对象所引用的所有对象都必须标记为 Serializable(请参阅基

正在进行序列化的对象所引用的所有对象都必须标记为 Serializable(请参阅基

本序列
化)。否则,当序列化程序试图序列化未标记的对象时将会出现异常。
当反序列化已序列化的类时,将重新创建该类,并自动还原所有数据成员的值。
按值封送
对象仅在创建对象的应用程序域中有效。除非对象是从 MarshalByRefObject 派生

得到
或标记为 Serializable,否则,任何将对象作为参数传递或将其作为结果返回的

尝试都
将失败。如果对象标记为 Serializable,则该对象将被自动序列化,并从一个应

用程序
域传输至另一个应用程序域,然后进行反序列化,从而在第二个应用程序域中产生

出该
对象的一个精确副本。此过程通常称为按值封送。
如果对象是从 MarshalByRefObject 派生得到,则从一个应用程序域传递至另一个

应用
程序域的是对象引用,而不是对象本身。也可以将从 MarshalByRefObject 派生得

到的
对象标记为 Serializable。远程使用此对象时,负责进行序列化并已预先配置为

 Surr
ogateSelector 的格式化程序将控制序列化过程,并用一个代理替换所有从
MarshalBy
RefObject 派生得到的对象。如果没有预先配置为 SurrogateSelector,序列化体

RefObject 派生得到的对象。如果没有预先配置为 SurrogateSelector,序列化体

系结
构将遵从下面的标准序列化规则(请参阅序列化过程的步骤)。
基本序列化
要使一个类可序列化,最简单的方法是使用 Serializable 属性对它进行标记,如

下所
示:
[Serializable]
public class MyObject {
  public int n1 = 0;
  public int n2 = 0;
  public String str = null;
}
以下代码片段说明了如何将此类的一个实例序列化为一个文件:
MyObject obj = new MyObject();
obj.n1 = 1;
obj.n2 = 24;
obj.str = "一些字符串";
IFormatter formatter = new BinaryFormatter();
Stream stream = new FileStream("MyFile.bin", FileMode.Create,
FileAccess.Write, FileShare.None);
formatter.Serialize(stream, obj);
stream.Close();
stream.Close();
本例使用二进制格式化程序进行序列化。您只需创建一个要使用的流和格式化程序

的实
例,然后调用格式化程序的 Serialize 方法。流和要序列化的对象实例作为参数

提供给
此调用。类中的所有成员变量(甚至标记为 private 的变量)都将被序列化,但

这一点
在本例中未明确体现出来。在这一点上,二进制序列化不同于只序列化公共字段的

 XML
 序列化程序。
将对象还原到它以前的状态也非常容易。首先,创建格式化程序和流以进行读取,

然后
让格式化程序对对象进行反序列化。以下代码片段说明了如何进行此操作。
IFormatter formatter = new BinaryFormatter();
Stream stream = new FileStream("MyFile.bin", FileMode.Open,
FileAccess.Read, FileShare.Read);
MyObject obj = (MyObject) formatter.Deserialize(fromStream);
stream.Close();
// 下面是证明
Console.WriteLine("n1: {0}", obj.n1);
Console.WriteLine("n2: {0}", obj.n2);
Console.WriteLine("str: {0}", obj.str);
上面所使用的 BinaryFormatter 效率很高,能生成非常紧凑的字节流。所有使用

上面所使用的 BinaryFormatter 效率很高,能生成非常紧凑的字节流。所有使用

此格式
化程序序列化的对象也可使用它进行反序列化,对于序列化将在 .NET 平台上进行

反序
列化的对象,此格式化程序无疑是一个理想工具。需要注意的是,对对象进行反序

列化
时并不调用构造函数。对反序列化添加这项约束,是出于性能方面的考虑。但是,

这违
反了对象编写者通常采用的一些运行时约定,因此,开发人员在将对象标记为可序

列化
时,应确保考虑了这一特殊约定。
如果要求具有可移植性,请使用 SoapFormatter。所要做的更改只是将以上代码中

的格
式化程序换成 SoapFormatter,而 Serialize 和 Deserialize 调用不变。对于上

面使
用的示例,该格式化程序将生成以下结果。

<SOAP-ENV:Envelope
  xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance
  xmlns:xsd="http://www.w3.org/2001/XMLSchema"
  xmlns:SOAP- ENC=http://schemas.xmlsoap.org/soap/encoding/
  xmlns:SOAP- ENV=http://schemas.xmlsoap.org/soap/envelope/
  SOAP-ENV:encodingStyle=
  SOAP-ENV:encodingStyle=
  "http://schemas.microsoft.com/soap/encoding/clr/1.0
  http://schemas.xmlsoap.org/soap/encoding/"
  xmlns:a1="http://schemas.microsoft.com/clr/assem/ToFile">
  <SOAP-ENV:Body>
    <a1:MyObject id="ref-1">
      <n1>1</n1>
      <n2>24</n2>
      <str id="ref-3">一些字符串</str>
    </a1:MyObject>
  </SOAP-ENV:Body>
</SOAP-ENV:Envelope>
需要注意的是,无法继承 Serializable 属性。如果从 MyObject 派生出一个新的

类,
则这个新的类也必须使用该属性进行标记,否则将无法序列化。例如,如果试图序

列化
以下类实例,将会显示一个 SerializationException,说明 MyStuff 类型未标记

为可
序列化。
public class MyStuff : MyObject
{
  public int n3;
}
}
使用序列化属性非常方便,但是它存在上述的一些限制。有关何时标记类以进行序

列化
(因为类编译后就无法再序列化),请参考有关说明(请参阅下面的序列化规则)


选择性序列化
类通常包含不应被序列化的字段。例如,假设某个类用一个成员变量来存储线程
ID。当
此类被反序列化时,序列化此类时所存储的 ID 对应的线程可能不再运行,所以对

这个
值进行序列化没有意义。可以通过使用 NonSerialized 属性标记成员变量来防止

它们被
序列化,如下所示:
[Serializable]
public class MyObject
{
  public int n1;
  [NonSerialized] public int n2;
  public String str;
}
自定义序列化
可以通过在对象上实现 ISerializable 接口来自定义序列化过程。这一功能在反

序列化
序列化
后成员变量的值失效时尤其有用,但是需要为变量提供值以重建对象的完整状态。

要实
现 ISerializable,需要实现 GetObjectData 方法以及一个特殊的构造函数,在

反序列
化对象时要用到此构造函数。以下代码示例说明了如何在前一部分中提到的
MyObject
类上实现 ISerializable。
[Serializable]
public class MyObject : ISerializable
{
  public int n1;
  public int n2;
  public String str;
  public MyObject()
  {
  }
  protected MyObject(SerializationInfo info, StreamingContext context)
  {
    n1 = info.GetInt32("i");
    n2 = info.GetInt32("j");
    str = info.GetString("k");
  }
  }
  public virtual void GetObjectData(SerializationInfo info,
StreamingContext context)
  {
    info.AddValue("i", n1);
    info.AddValue("j", n2);
    info.AddValue("k", str);
  }
}
在序列化过程中调用 GetObjectData 时,需要填充方法调用中提供的
SerializationI
nfo 对象。只需按名称/值对的形式添加将要序列化的变量。其名称可以是任何文

本。只
要已序列化的数据足以在反序列化过程中还原对象,便可以自由选择添加至
Serializa
tionInfo 的成员变量。如果基对象实现了 ISerializable,则派生类应调用其基

对象的
 GetObjectData 方法。
需要强调的是,将 ISerializable 添加至某个类时,需要同时实现
GetObjectData 以
及特殊的构造函数。如果缺少 GetObjectData,编译器将发出警告。但是,由于无

法强
制实现构造函数,所以,缺少构造函数时不会发出警告。如果在没有构造函数的情

制实现构造函数,所以,缺少构造函数时不会发出警告。如果在没有构造函数的情

况下
尝试反序列化某个类,将会出现异常。在消除潜在安全性和版本控制问题等方面,

当前
设计优于 SetObjectData 方法。例如,如果将 SetObjectData 方法定义为某个接

口的
一部分,则此方法必须是公共方法,这使得用户不得不编写代码来防止多次调用
SetOb
jectData 方法。可以想象,如果某个对象正在执行某些操作,而某个恶意应用程

序却调
用此对象的 SetObjectData 方法,将会引起一些潜在的麻烦。
在反序列化过程中,使用出于此目的而提供的构造函数将 SerializationInfo 传

递给类
。对象反序列化时,对构造函数的任何可见性约束都将被忽略,因此,可以将类标

记为
 public、protected、internal 或 private。一个不错的办法是,在类未封装的

情况下
,将构造函数标记为 protect。如果类已封装,则应标记为 private。要还原对象

的状
态,只需使用序列化时采用的名称,从 SerializationInfo 中检索变量的值。如

果基类
实现了 ISerializable,则应调用基类的构造函数,以使基础对象可以还原其变量



如果从实现了 ISerializable 的类派生出一个新的类,则只要新的类中含有任何

需要序
列化的变量,就必须同时实现构造函数以及 GetObjectData 方法。以下代码片段

显示了
如何使用上文所示的 MyObject 类来完成此操作。
[Serializable]
public class ObjectTwo : MyObject
{
  public int num;
  public ObjectTwo() : base()
  {
  }
  protected ObjectTwo(SerializationInfo si, StreamingContext context) :


base(si,context)
  {
    num = si.GetInt32("num");
  }
  public override void GetObjectData(SerializationInfo si,
StreamingContext context)
  {
    base.GetObjectData(si,context);
    base.GetObjectData(si,context);
    si.AddValue("num", num);
  }
}
切记要在反序列化构造函数中调用基类,否则,将永远不会调用基类上的构造函数

,并
且在反序列化后也无法构建完整的对象。
对象被彻底重新构建,但是在反系列化过程中调用方法可能会带来不良的副作用,

因为被
调用的方法可能引用了在调用时尚未反序列化的对象引用。如果正在进行反序列化

的类

实现了 IDeserializationCallback,则反序列化整个对象图表后,将自动调用
OnSeri
alization 方法。此时,引用的所有子对象均已完全还原。有些类不使用上述事件

侦听
器,很难对它们进行反序列化,散列表便是一个典型的例子。在反序列化过程中检

索关
键字/值对非常容易,但是,由于无法保证从散列表派生出的类已反序列化,所以

把这些
对象添加回散列表时会出现一些问题。因此,建议目前不要在散列表上调用方法。


序列化过程的步骤
序列化过程的步骤
在格式化程序上调用 Serialize 方法时,对象序列化按照以下规则进行:
检查格式化程序是否有代理选取器。如果有,检查代理选取器是否处理指定类型的

对象
。如果选取器处理此对象类型,将在代理选取器上调用 ISerializable.
GetObjectData

如果没有代理选取器或有却不处理此类型,将检查是否使用 Serializable 属性对

对象
进行标记。如果未标记,将会引发 SerializationException。
如果对象已被正确标记,将检查对象是否实现了 ISerializable。如果已实现,将

在对
象上调用 GetObjectData。
如果对象未实现 Serializable,将使用默认的序列化策略,对所有未标记为
NonSeria
lized 的字段都进行序列化。
版本控制
.NET 框架支持版本控制和并排执行,并且,如果类的接口保持一致,所有类均可

跨版本
工作。由于序列化涉及的是成员变量而非接口,所以,在向要跨版本序列化的类中

添加
成员变量,或从中删除变量时,应谨慎行事。特别是对于未实现 ISerializable
的类更
的类更
应如此。若当前版本的状态发生了任何变化(例如添加成员变量、更改变量类型或

更改
变量名称),都意味着如果同一类型的现有对象是使用早期版本进行序列化的,则

无法
成功对它们进行反序列化。
如果对象的状态需要在不同版本间发生改变,类的作者可以有两种选择:
实现 ISerializable。这使您可以精确地控制序列化和反序列化过程,在反序列化

过程
中正确地添加和解释未来状态。
使用 NonSerialized 属性标记不重要的成员变量。仅当预计类在不同版本间的变

化较小
时,才可使用这个选项。例如,把一个新变量添加至类的较高版本后,可以将该变

量标
记为 NonSerialized,以确保该类与早期版本保持兼容。
序列化规则
由于类编译后便无法序列化,所以在设计新类时应考虑序列化。需要考虑的问题有

:是
否必须跨应用程序域来发送此类?是否要远程使用此类?用户将如何使用此类?也

许他
们会从我的类中派生出一个需要序列化的新类。只要有这种可能性,就应将类标记

为可
序列化。除下列情况以外,最好将所有类都标记为可序列化:
由于类编译后便无法序列化,所以在设计新类时应考虑序列化。需要考虑的问题有

所有的类都永远也不会跨越应用程序域。如果某个类不要求序列化但需要跨越应用

程序
域,请从 MarshalByRefObject 派生此类。
类存储仅适用于其当前实例的特殊指针。例如,如果某个类包含非受控的内存或文

件句
柄,请确保将这些字段标记为 NonSerialized 或根本不序列化此类。
某些数据成员包含敏感信息。在这种情况下,建议实现 ISerializable 并仅序列

化所要
求的字段。

--
     来 人
      Welcome to Sunrise!

             我总有一种想为你而死的冲动

                          因为我不知如何才能把你打动

※ 来源:·荔园晨风BBS站 bbs.szu.edu.cn·[FROM: 192.168.52.238]


[回到开始] [上一篇][下一篇]

荔园在线首页 友情链接:深圳大学 深大招生 荔园晨风BBS S-Term软件 网络书店