C# Serializable
2.
Serializable在C#中的做用.NET 中的对象序列化
简介
序列化是指将对象实例的状态存储到存储媒体的过程。在此过程当中,先将对象的公共字段和私有字段以及类的名称(包括类所在的程序集)转换为字节流,而后再把字节流写入数据流。在随后对对象进行反序列化时,将建立出与原对象彻底相同的副本。
在面向对象的环境中实现序列化机制时,必须在易用性和灵活性之间进行一些权衡。只要您对此过程有足够的控制能力,就可使该过程在很大程度上自动进行。例如,简单的二进制序列化不能知足须要,或者,因为特定缘由须要肯定类中那些字段须要序列化。如下各部分将探讨 .NET 框架提供的可靠的序列化机制,并着重介绍使您能够根据须要自定义序列化过程的一些重要功能。
持久存储
咱们常常须要将对象的字段值保存到磁盘中,并在之后检索此数据。尽管不使用序列化也能完成这项工做,但这种方法一般很繁琐并且容易出错,而且在须要跟踪对象的层次结构时,会变得愈来愈复杂。能够想象一下编写包含大量对象的大型业务应用程序的情形,程序员不得不为每个对象编写代码,以便将字段和属性保存至磁盘以及从磁盘还原这些字段和属性。序列化提供了轻松实现这个目标的快捷方法。
公共语言运行时 (CLR) 管理对象在内存中的分布,.NET 框架则经过使用反射提供自动的序列化机制。对象序列化后,类的名称、程序集以及类实例的全部数据成员均被写入存储媒体中。对象一般用成员变量来存储对其余实例的引用。类序列化后,序列化引擎将跟踪全部已序列化的引用对象,以确保同一对象不被序列化屡次。.NET 框架所提供的序列化体系结构能够自动正确处理对象图表和循环引用。对对象图表的惟一要求是,由正在进行序列化的对象所引用的全部对象都必须标记为 Serializable(请参阅基本序列化)。不然,当序列化程序试图序列化未标记的对象时将会出现异常。
当反序列化已序列化的类时,将从新建立该类,并自动还原全部数据成员的值。
按值封送
对象仅在建立对象的应用程序域中有效。除非对象是从 MarshalByRefObject 派生获得或标记为 Serializable,不然,任何将对象做为参数传递或将其做为结果返回的尝试都将失败。若是对象标记为 Serializable,则该对象将被自动序列化,并从一个应用程序域传输至另外一个应用程序域,而后进行反序列化,从而在第二个应用程序域中产生出该对象的一个精确副本。此过程一般称为按值封送。
若是对象是从 MarshalByRefObject 派生获得,则从一个应用程序域传递至另外一个应用程序域的是对象引用,而不是对象自己。也能够将从 MarshalByRefObject 派生获得的对象标记为 Serializable。远程使用此对象时,负责进行序列化并已预先配置为 SurrogateSelector 的格式化程序将控制序列化过程,并用一个代理替换全部从 MarshalByRefObject 派生获得的对象。若是没有预先配置为 SurrogateSelector,序列化体系结构将听从下面的标准序列化规则(请参阅序列化过程的步骤)。
基本序列化
要使一个类可序列化,最简单的方法是使用 Serializable 属性对它进行标记,以下所示:
[Serializable] 
public
class
MyObject
...
{
publicint n1 =0;
publicint 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();
本例使用二进制格式化程序进行序列化。您只需建立一个要使用的流和格式化程序的实例,而后调用格式化程序的 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 效率很高,能生成很是紧凑的字节流。全部使用此格式化程序序列化的对象也可以使用它进行反序列化,对于序列化将在 .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
=
"
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 
...
{
publicint n3;
}
使用序列化属性很是方便,可是它存在上述的一些限制。有关什么时候标记类以进行序列化(由于类编译后就没法再序列化),请参考有关说明(请参阅下面的序列化规则)。
选择性序列化
类一般包含不该被序列化的字段。例如,假设某个类用一个成员变量来存储线程 ID。当此类被反序列化时,序列化此类时所存储的 ID 对应的线程可能再也不运行,因此对这个值进行序列化没有意义。能够经过使用 NonSerialized 属性标记成员变量来防止它们被序列化,以下所示:
[Serializable]
public
class
MyObject 
...
{
publicint n1;
[NonSerialized] publicint n2;
public String str;
}
自定义序列化
能够经过在对象上实现 ISerializable 接口来自定义序列化过程。这一功能在反序列化后成员变量的值失效时尤为有用,可是须要为变量提供值以重建对象的完整状态。要实现 ISerializable,须要实现 GetObjectData 方法以及一个特殊的构造函数,在反序列化对象时要用到此构造函数。如下代码示例说明了如何在前一部分中提到的 MyObject 类上实现 ISerializable。
[Serializable]
public
class
MyObject : ISerializable 
...
{
publicint n1;
publicint n2;
public String str;
public MyObject() 
...{
}
protected MyObject(SerializationInfo info, StreamingContext context) 
...{
n1 = info.GetInt32("i");
n2 = info.GetInt32("j");
str = info.GetString("k");
}
publicvirtualvoid GetObjectData(SerializationInfo info,
StreamingContext context) 
...{
info.AddValue("i", n1);
info.AddValue("j", n2);
info.AddValue("k", str);
}
}
在序列化过程当中调用 GetObjectData 时,须要填充方法调用中提供的 SerializationInfo 对象。只需按名称
/
值对的形式添加将要序列化的变量。其名称能够是任何文本。只要已序列化的数据足以在反序列化过程当中还原对象,即可以自由选择添加至 SerializationInfo 的成员变量。若是基对象实现了 ISerializable,则派生类应调用其基对象的 GetObjectData 方法。
须要强调的是,将 ISerializable 添加至某个类时,须要同时实现 GetObjectData 以及特殊的构造函数。若是缺乏 GetObjectData,编译器将发出警告。可是,因为没法强制实现构造函数,因此,缺乏构造函数时不会发出警告。若是在没有构造函数的状况下尝试反序列化某个类,将会出现异常。在消除潜在安全性和版本控制问题等方面,当前设计优于 SetObjectData 方法。例如,若是将 SetObjectData 方法定义为某个接口的一部分,则此方法必须是公共方法,这使得用户不得不编写代码来防止屡次调用 SetObjectData 方法。能够想象,若是某个对象正在执行某些操做,而某个恶意应用程序却调用此对象的 SetObjectData 方法,将会引发一些潜在的麻烦。
在反序列化过程当中,使用出于此目的而提供的构造函数将 SerializationInfo 传递给类。对象反序列化时,对构造函数的任何可见性约束都将被忽略,所以,能够将类标记为
public
、
protected
、
internal
或
private
。一个不错的办法是,在类未封装的状况下,将构造函数标记为 protect。若是类已封装,则应标记为
private
。要还原对象的状态,只需使用序列化时采用的名称,从 SerializationInfo 中检索变量的值。若是基类实现了 ISerializable,则应调用基类的构造函数,以使基础对象能够还原其变量。
若是从实现了 ISerializable 的类派生出一个新的类,则只要新的类中含有任何须要序列化的变量,就必须同时实现构造函数以及 GetObjectData 方法。如下代码片断显示了如何使用上文所示的 MyObject 类来完成此操做。
[Serializable]
public
class
ObjectTwo : MyObject 
...
{
publicint num;
public ObjectTwo() : base() 
...{
}
protected ObjectTwo(SerializationInfo si, StreamingContext context) :
base(si,context) 
...{
num = si.GetInt32("num");
}
publicoverridevoid GetObjectData(SerializationInfo si,
StreamingContext context) 
...{
base.GetObjectData(si,context);
si.AddValue("num", num);
}
}
切记要在反序列化构造函数中调用基类,不然,将永远不会调用基类上的构造函数,而且在反序列化后也没法构建完整的对象。
对象被完全从新构建,可是在反系列化过程当中调用方法可能会带来不良的反作用,由于被调用的方法可能引用了在调用时还没有反序列化的对象引用。若是正在进行反序列化的类实现了 IDeserializationCallback,则反序列化整个对象图表后,将自动调用 OnSerialization 方法。此时,引用的全部子对象均已彻底还原。有些类不使用上述事件侦听器,很难对它们进行反序列化,散列表即是一个典型的例子。在反序列化过程当中检索关键字
/
值对很是容易,可是,因为没法保证从散列表派生出的类已反序列化,因此把这些对象添加回散列表时会出现一些问题。所以,建议目前不要在散列表上调用方法。
序列化过程的步骤
在格式化程序上调用 Serialize 方法时,对象序列化按照如下规则进行:
检查格式化程序是否有代理选取器。若是有,检查代理选取器是否处理指定类型的对象。若是选取器处理此对象类型,将在代理选取器上调用 ISerializable.GetObjectData。
若是没有代理选取器或有却不处理此类型,将检查是否使用 Serializable 属性对对象进行标记。若是未标记,将会引起 SerializationException。
若是对象已被正确标记,将检查对象是否实现了 ISerializable。若是已实现,将在对象上调用 GetObjectData。
若是对象未实现 Serializable,将使用默认的序列化策略,对全部未标记为 NonSerialized 的字段都进行序列化。
版本控制
.NET 框架支持版本控制和并排执行,而且,若是类的接口保持一致,全部类都可跨版本工做。因为序列化涉及的是成员变量而非接口,因此,在向要跨版本序列化的类中添加成员变量,或从中删除变量时,应谨慎行事。特别是对于未实现 ISerializable 的类更应如此。若当前版本的状态发生了任何变化(例如添加成员变量、更改变量类型或更改变量名称),都意味着若是同一类型的现有对象是使用早期版本进行序列化的,则没法成功对它们进行反序列化。
若是对象的状态须要在不一样版本间发生改变,类的做者能够有两种选择:
实现 ISerializable。这使您能够精确地控制序列化和反序列化过程,在反序列化过程当中正确地添加和解释将来状态。
使用 NonSerialized 属性标记不重要的成员变量。仅当预计类在不一样版本间的变化较小时,才可以使用这个选项。例如,把一个新变量添加至类的较高版本后,能够将该变量标记为 NonSerialized,以确保该类与早期版本保持兼容。
序列化规则
因为类编译后便没法序列化,因此在设计新类时应考虑序列化。须要考虑的问题有:是否必须跨应用程序域来发送此类?是否要远程使用此类?用户将如何使用此类?也许他们会从个人类中派生出一个须要序列化的新类。只要有这种可能性,就应将类标记为可序列化。除下列状况之外,最好将全部类都标记为可序列化:
全部的类都永远也不会跨越应用程序域。若是某个类不要求序列化但须要跨越应用程序域,请从 MarshalByRefObject 派生此类。
类存储仅适用于其当前实例的特殊指针。例如,若是某个类包含非受控的内存或文件句柄,请确保将这些字段标记为 NonSerialized 或根本不序列化此类。
某些数据成员包含敏感信息。在这种状况下,建议实现 ISerializable 并仅序列化所要求的字段。
Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=1735611